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 = { authUser: '00000000-0000-0000-0000-00000000a101', authTenantAdminUser: '00000000-0000-0000-0000-00000000a102', authTenantOperatorUser: '00000000-0000-0000-0000-00000000a103', authPlatformAdminUser: '00000000-0000-0000-0000-00000000a999', 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', tenantTeacherUser: '00000000-0000-0000-0000-000000000106', secondStudentUser: '00000000-0000-0000-0000-000000000107', region: '00000000-0000-0000-0000-000000000301', subject: '00000000-0000-0000-0000-000000000501', category: '00000000-0000-0000-0000-000000000601', contentEntry: '00000000-0000-0000-0000-000000000611', contentNodeCulture: '00000000-0000-0000-0000-000000000612', contentNodeProfessional: '00000000-0000-0000-0000-000000000613', contentNodeSchoolTarget: '00000000-0000-0000-0000-000000000614', questionCollection: '00000000-0000-0000-0000-000000000615', practiceBlueprintSequential: '00000000-0000-0000-0000-000000000616', practiceBlueprintRandom: '00000000-0000-0000-0000-000000000617', practiceBlueprintMock: '00000000-0000-0000-0000-000000000618', leaderboardSessionUser: '00000000-0000-0000-0000-000000000621', leaderboardSessionSecond: '00000000-0000-0000-0000-000000000622', leaderboardReportUser: '00000000-0000-0000-0000-000000000623', leaderboardReportSecond: '00000000-0000-0000-0000-000000000624', leaderboardAnswerUserOne: '00000000-0000-0000-0000-000000000625', leaderboardAnswerUserTwo: '00000000-0000-0000-0000-000000000626', leaderboardAnswerSecondOne: '00000000-0000-0000-0000-000000000627', leaderboardAnswerSecondTwo: '00000000-0000-0000-0000-000000000628', leaderboardScoreEventSecond: '00000000-0000-0000-0000-000000000629', questionBank: '00000000-0000-0000-0000-000000000400', question: '00000000-0000-0000-0000-000000000401', questionVersion: '00000000-0000-0000-0000-000000000402', questionTwo: '00000000-0000-0000-0000-000000000403', questionTwoVersion: '00000000-0000-0000-0000-000000000404', questionThree: '00000000-0000-0000-0000-000000000405', questionThreeVersion: '00000000-0000-0000-0000-000000000406', plan: '00000000-0000-0000-0000-000000000201', couponOnlyPlan: '00000000-0000-0000-0000-000000000202', checkoutCoupon: '00000000-0000-0000-0000-000000000203', freeCheckoutCoupon: '00000000-0000-0000-0000-000000000204', order: '00000000-0000-0000-0000-000000000701', payment: '00000000-0000-0000-0000-000000000702', activationCode: '00000000-0000-0000-0000-000000000801', selfActivationCode: '00000000-0000-0000-0000-000000000802', vocabularyUnit: '00000000-0000-0000-0000-000000000811', vocabularyWord: '00000000-0000-0000-0000-000000000812', vocabularyWordTwo: '00000000-0000-0000-0000-000000000813', video: '00000000-0000-0000-0000-000000000821', questionVideo: '00000000-0000-0000-0000-000000000822', videoAsset: '00000000-0000-0000-0000-000000000823', quotaVideo: '00000000-0000-0000-0000-000000000824', quotaQuestionVideo: '00000000-0000-0000-0000-000000000825', videoQuotaAccount: '00000000-0000-0000-0000-000000000826', 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', tenantClass: '00000000-0000-0000-0000-000000000851', tenantClassOther: '00000000-0000-0000-0000-000000000852', examDate: '00000000-0000-0000-0000-000000000861', examDateSchool: '00000000-0000-0000-0000-000000000862', tenantBadge: '00000000-0000-0000-0000-000000000871', tenantCheckInBadge: '00000000-0000-0000-0000-000000000873', tenantScoreBadge: '00000000-0000-0000-0000-000000000874', tenantFeedbackBadge: '00000000-0000-0000-0000-000000000875', tenantActivityBadge: '00000000-0000-0000-0000-000000000876', tenantPracticeBadge: '00000000-0000-0000-0000-000000000881', tenantVocabularyBadge: '00000000-0000-0000-0000-000000000882', tenantMockExamBadge: '00000000-0000-0000-0000-000000000883', dailyCheckInTask: '00000000-0000-0000-0000-000000000884', streakCheckInTask: '00000000-0000-0000-0000-000000000885', pointActivityTask: '00000000-0000-0000-0000-000000000877', pointSystemTask: '00000000-0000-0000-0000-000000000878', pointExchangeItem: '00000000-0000-0000-0000-000000000879', pointExpensiveExchangeItem: '00000000-0000-0000-0000-000000000880', 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', publicQuestionBankGrant: '00000000-0000-0000-0000-000000000906', partnerTenantAdminUser: '00000000-0000-0000-0000-000000000907', platformAdminUser: '00000000-0000-0000-0000-000000000999', }; const pool = new Pool({ connectionString: databaseUrl }); async function main() { const client = await pool.connect(); try { await client.query('begin'); await client.query( ` create temporary table smoke_seed_transient_questions ( id uuid primary key ) on commit drop `, ); await client.query( ` insert into smoke_seed_transient_questions (id) select distinct q.id from public.questions q left join public.question_collection_items ci on ci.tenant_id = q.tenant_id and ci.question_id = q.id where q.tenant_id = $1 and q.id <> all($3::uuid[]) and ( q.primary_collection_id = $2::uuid or ci.collection_id = $2::uuid or coalesce(q.legacy_id, '') like any($4::text[]) ) `, [ tenantId, ids.questionCollection, [ids.question, ids.questionTwo, ids.questionThree], ['integration-import-%', 'worker-import-question-%', 'public-sync-source-%', 'content-import:%'], ], ); await client.query( ` delete from public.question_collection_items where tenant_id = $1 and ( question_id in (select id from smoke_seed_transient_questions) or ( collection_id = $2::uuid and question_id <> all($3::uuid[]) ) ) `, [tenantId, ids.questionCollection, [ids.question, ids.questionTwo, ids.questionThree]], ); await client.query( ` delete from public.question_versions where tenant_id = $1 and question_id in (select id from smoke_seed_transient_questions) `, [tenantId], ); await client.query( ` delete from public.questions where tenant_id = $1 and id in (select id from smoke_seed_transient_questions) `, [tenantId], ); await client.query( ` delete from public.content_export_jobs where tenant_id = $1 and scope_id = $2::uuid `, [tenantId, ids.questionCollection], ); await client.query( ` delete from public.content_import_jobs where tenant_id = $1 and ( source_name = any($2::text[]) or source_name like 'worker-import-%' ) `, [ tenantId, [ 'large-but-allowed-question-import.json', 'invalid-question-import.json', 'valid-question-import.json', 'questions.csv', 'async-question-import.json', 'invalid-vocabulary-import.json', 'legacy-vocabulary-import.json', 'vocabulary.csv', 'handbook-nested-import.json', 'scoreline-batch-import.json', 'scoreline-mixed-items.json', 'scoreline.xlsx', 'video-batch-import.json', ], ], ); await client.query( ` delete from public.user_notifications where tenant_id = $1 and ( user_id in ($2::uuid, $3::uuid) or dedupe_key like any($4::text[]) or source_type in ('reports', 'user_badges', 'user_point_exchange_orders') ) `, [ tenantId, ids.user, ids.secondStudentUser, ['feedback_%', 'badge:%', 'point_exchange:%'], ], ); await client.query( ` delete from public.report_status_events where tenant_id = $1 and report_id in ( select id from public.reports where tenant_id = $1 and ( user_id in ($2::uuid, $3::uuid) or question_id in ($4::uuid, $5::uuid) ) ) `, [tenantId, ids.user, ids.tenantAdminUser, ids.question, ids.questionTwo], ); await client.query( ` delete from public.reports where tenant_id = $1 and ( user_id in ($2::uuid, $3::uuid) or question_id in ($4::uuid, $5::uuid) ) `, [tenantId, ids.user, ids.tenantAdminUser, ids.question, ids.questionTwo], ); await client.query( ` delete from public.user_point_exchange_orders where tenant_id = $1 and ( user_id in ($2::uuid, $3::uuid) or item_id = any($4::uuid[]) or idempotency_key like 'integration-%' ) `, [ tenantId, ids.user, ids.secondStudentUser, [ids.pointExchangeItem, ids.pointExpensiveExchangeItem], ], ); await client.query( ` delete from public.user_point_activity_claims where tenant_id = $1 and ( user_id in ($2::uuid, $3::uuid) or task_id = any($4::uuid[]) or coalesce(metadata->>'source', '') = 'integration-test' ) `, [ tenantId, ids.user, ids.secondStudentUser, [ids.pointActivityTask, ids.pointSystemTask, ids.dailyCheckInTask, ids.streakCheckInTask], ], ); await client.query( ` delete from public.user_score_events where tenant_id = $1 and ( user_id in ($2::uuid, $3::uuid, $4::uuid) or id = $5::uuid ) `, [tenantId, ids.user, ids.tenantAdminUser, ids.secondStudentUser, ids.leaderboardScoreEventSecond], ); await client.query( ` delete from public.point_exchange_items where tenant_id = $1 and ( id = any($2::uuid[]) or coalesce(legacy_id, '') like 'integration-point-exchange-%' or code::text like 'integration-%' ) `, [tenantId, [ids.pointExchangeItem, ids.pointExpensiveExchangeItem]], ); await client.query( ` delete from public.point_activity_tasks where tenant_id = $1 and ( id = any($2::uuid[]) or coalesce(legacy_id, '') like 'integration-point-task-%' or code::text like 'integration-%' ) `, [tenantId, [ids.pointActivityTask, ids.pointSystemTask, ids.dailyCheckInTask, ids.streakCheckInTask]], ); await client.query( ` delete from public.audit_logs where tenant_id = $1 and target_type = 'ai_recommendation_report' and target_id in ( select id::text from public.ai_recommendation_reports where tenant_id = $1 and user_id in ($2::uuid, $3::uuid) ) `, [tenantId, ids.user, ids.secondStudentUser], ); await client.query( ` delete from public.ai_recommendation_reports where tenant_id = $1 and user_id in ($2::uuid, $3::uuid) `, [tenantId, ids.user, ids.secondStudentUser], ); await client.query( ` delete from public.user_badges where tenant_id = $1 and ( user_id in ($2::uuid, $3::uuid) or badge_id = any($4::uuid[]) or coalesce(legacy_id, '') = any($5::text[]) ) `, [ tenantId, ids.user, ids.secondStudentUser, [ ids.tenantBadge, ids.tenantCheckInBadge, ids.tenantScoreBadge, ids.tenantFeedbackBadge, ids.tenantActivityBadge, ids.tenantPracticeBadge, ids.tenantVocabularyBadge, ids.tenantMockExamBadge, ], [ `badge:${ids.tenantBadge}:user:${ids.user}`, `auto:${ids.tenantCheckInBadge}:user:${ids.user}`, `auto:${ids.tenantScoreBadge}:user:${ids.user}`, `auto:${ids.tenantFeedbackBadge}:user:${ids.user}`, `auto:${ids.tenantActivityBadge}:user:${ids.user}`, `auto:${ids.tenantPracticeBadge}:user:${ids.user}`, `auto:${ids.tenantPracticeBadge}:user:${ids.tenantAdminUser}`, `auto:${ids.tenantVocabularyBadge}:user:${ids.user}`, `auto:${ids.tenantMockExamBadge}:user:${ids.user}`, `auto:${ids.tenantMockExamBadge}:user:${ids.tenantAdminUser}`, ], ], ); await client.query( ` delete from public.badges where tenant_id = $1 and ( id = any($2::uuid[]) or coalesce(legacy_id, '') like 'integration-badge-%' ) `, [ tenantId, [ ids.tenantBadge, ids.tenantCheckInBadge, ids.tenantScoreBadge, ids.tenantFeedbackBadge, ids.tenantActivityBadge, ids.tenantPracticeBadge, ids.tenantVocabularyBadge, ids.tenantMockExamBadge, ], ], ); await client.query( ` delete from public.practice_session_report_sections where tenant_id = $1 and report_id in ($2::uuid, $3::uuid) `, [tenantId, ids.leaderboardReportUser, ids.leaderboardReportSecond], ); await client.query( ` delete from public.practice_session_reports where tenant_id = $1 and id in ($2::uuid, $3::uuid) `, [tenantId, ids.leaderboardReportUser, ids.leaderboardReportSecond], ); await client.query( ` delete from public.answer_records where tenant_id = $1 and id in ($2::uuid, $3::uuid, $4::uuid, $5::uuid) `, [ tenantId, ids.leaderboardAnswerUserOne, ids.leaderboardAnswerUserTwo, ids.leaderboardAnswerSecondOne, ids.leaderboardAnswerSecondTwo, ], ); await client.query( ` delete from public.practice_sessions where tenant_id = $1 and id in ($2::uuid, $3::uuid) `, [tenantId, ids.leaderboardSessionUser, ids.leaderboardSessionSecond], ); const checkoutCouponCodes = [ 'SMOKE50', 'SMOKEFREE', 'IT-COUPON-001', 'IT-POINT-EXCHANGE-COUPON', ]; await client.query( ` delete from public.coupon_redemptions where tenant_id = $1 and ( coupon_id = any($2::uuid[]) or coupon_code = any($3::text[]) or coupon_code like any($4::text[]) or coupon_id in ( select id from public.coupons where tenant_id = $1 and ( campaign_name in ('integration-coupon-rules', 'integration-point-exchange', 'tenant-admin-coupon-smoke') or code::text like any($4::text[]) ) ) or order_id in ( select id from public.orders where tenant_id = $1 and ( (raw_payload->'pricing'->>'couponCode') = any($3::text[]) or (raw_payload->'pricing'->>'couponCode') like any($4::text[]) ) ) ) `, [ tenantId, [ids.checkoutCoupon, ids.freeCheckoutCoupon], checkoutCouponCodes, ['ITMIN%', 'ITDIS%', 'ITREG%', 'ITMULTI%', 'ITFIRST%', 'IT-POINT-%'], ], ); await client.query( ` delete from public.order_items where tenant_id = $1 and order_id in ( select id from public.orders where tenant_id = $1 and (raw_payload->'pricing'->>'couponCode') = any($2::text[]) ) `, [tenantId, checkoutCouponCodes], ); await client.query( ` delete from public.entitlements where tenant_id = $1 and user_id = $2::uuid and source_type in ('order', 'activation_code') and ( legacy_source_id in ('SMOKE20260621', 'SMOKESELF20260621', 'IT-COMMISSION-001') or source_id in ( select id from public.orders where tenant_id = $1 and ( raw_payload->'pricing'->>'couponCode' = any($3::text[]) or order_no like 'SVIP%' or order_no like 'XP%' ) ) or metadata->>'orderNo' like 'SVIP%' or metadata->>'orderNo' like 'XP%' ) `, [tenantId, ids.user, checkoutCouponCodes], ); await client.query( ` delete from public.payments where tenant_id = $1 and order_id in ( select id from public.orders where tenant_id = $1 and ( (raw_payload->'pricing'->>'couponCode') = any($2::text[]) or order_no like 'SVIP%' or order_no like 'XP%' ) ) `, [tenantId, checkoutCouponCodes], ); await client.query( ` delete from public.orders where tenant_id = $1 and ( (raw_payload->'pricing'->>'couponCode') = any($2::text[]) or order_no like 'SVIP%' or order_no like 'XP%' ) `, [tenantId, checkoutCouponCodes], ); await client.query( ` delete from public.coupons where tenant_id = $1 and ( id = any($2::uuid[]) or code::text = any($3::text[]) or code::text like any($4::text[]) or campaign_name in ('integration-coupon-rules', 'integration-point-exchange', 'tenant-admin-coupon-smoke') ) `, [ tenantId, [ids.checkoutCoupon, ids.freeCheckoutCoupon], checkoutCouponCodes, ['ITMIN%', 'ITDIS%', 'ITREG%', 'ITMULTI%', 'ITFIRST%', 'IT-POINT-%'], ], ); await client.query( ` delete from public.commission_settlement_items where tenant_id = $1 and ( referrer_user_id in ($2::uuid, $3::uuid, $4::uuid) or student_user_id in ($5::uuid, $6::uuid) ) `, [tenantId, ids.tenantOperatorUser, ids.tenantSalesUser, ids.tenantAgentUser, ids.user, ids.secondStudentUser], ); await client.query( ` delete from public.commission_settlements where tenant_id = $1 and referrer_user_id in ($2::uuid, $3::uuid, $4::uuid) `, [tenantId, ids.tenantOperatorUser, ids.tenantSalesUser, ids.tenantAgentUser], ); await client.query( ` delete from public.tenant_commission_settings where tenant_id = $1 `, [tenantId], ); await client.query( ` delete from public.activation_codes where tenant_id = $1 and code::text = 'IT-COMMISSION-001' `, [tenantId], ); await client.query( ` delete from public.code_batches where tenant_id = $1 and name = '集成测试分佣批次' `, [tenantId], ); 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 idempotency_key = 'lead:worker-integration' 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.crm_webhook_log where tenant_id = $1 and ( record_id in ($2::text, $3::text, $4::text, $5::text) or response_summary like '%worker%' ) `, [tenantId, ids.user, ids.tenantOperatorUser, ids.tenantSalesUser, ids.tenantAgentUser], ); await client.query( ` with transient_users as ( select u.id from public.platform_users u where u.phone in ('13800000006', '13800000007', '13800000008') ) delete from public.practice_access_events where tenant_id = $1 and user_id in (select id from transient_users) `, [tenantId], ); await client.query( ` with transient_users as ( select u.id from public.platform_users u where u.phone in ('13800000006', '13800000007', '13800000008') ) delete from public.practice_daily_usage where tenant_id = $1 and user_id in (select id from transient_users) `, [tenantId], ); await client.query( ` with transient_users as ( select u.id from public.platform_users u where u.phone in ('13800000006', '13800000007', '13800000008') ) delete from public.answer_records where tenant_id = $1 and user_id in (select id from transient_users) `, [tenantId], ); await client.query( ` with transient_users as ( select u.id from public.platform_users u where u.phone in ('13800000006', '13800000007', '13800000008') ) delete from public.practice_sessions where tenant_id = $1 and user_id in (select id from transient_users) `, [tenantId], ); await client.query( ` with transient_users as ( select u.id from public.platform_users u where u.phone in ('13800000006', '13800000007', '13800000008') ) delete from app_private.auth_sessions where tenant_id = $1 and user_id in (select id from transient_users) `, [tenantId], ); await client.query( ` with transient_users as ( select u.id from public.platform_users u where u.phone in ('13800000006', '13800000007', '13800000008') ) delete from public.user_identities where user_id in (select id from transient_users) `, ); await client.query( ` delete from public.sms_verification_codes where tenant_id = $1 and phone in ('13800000006', '13800000007', '13800000008') `, [tenantId], ); 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, $5::uuid) or leader_user_id in ($2::uuid, $3::uuid, $4::uuid, $5::uuid) ) `, [tenantId, ids.tenantOperatorUser, ids.tenantSalesUser, ids.tenantAgentUser, ids.tenantTeacherUser], ); await client.query( ` insert into auth.users ( id, aud, role, phone, phone_confirmed_at, raw_app_meta_data, raw_user_meta_data, created_at, updated_at ) values ($1, 'authenticated', 'authenticated', '13800000000', now(), '{"provider":"phone","providers":["phone"]}'::jsonb, '{}'::jsonb, now(), now()), ($2, 'authenticated', 'authenticated', '13800000001', now(), '{"provider":"phone","providers":["phone"]}'::jsonb, '{}'::jsonb, now(), now()), ($3, 'authenticated', 'authenticated', '13800000003', now(), '{"provider":"phone","providers":["phone"]}'::jsonb, '{}'::jsonb, now(), now()), ($4, 'authenticated', 'authenticated', '13999999999', now(), '{"provider":"phone","providers":["phone"],"app_role":"platform_admin"}'::jsonb, '{}'::jsonb, now(), now()) on conflict (id) do update set phone = excluded.phone, raw_app_meta_data = excluded.raw_app_meta_data, updated_at = now() `, [ids.authUser, ids.authTenantAdminUser, ids.authTenantOperatorUser, ids.authPlatformAdminUser], ); await client.query( ` insert into public.platform_users (id, auth_user_id, username, phone, name, primary_role, raw_profile) values ($1, $2, 'smoke_student', '13800000000', 'Smoke Student', 'student', '{"source":"smoke-seed"}'::jsonb) on conflict (id) do update set username = excluded.username, auth_user_id = excluded.auth_user_id, phone = excluded.phone, name = excluded.name, updated_at = now() `, [ids.user, ids.authUser], ); await client.query( ` update public.platform_users set score = 0, updated_at = now() where id in ($1::uuid, $2::uuid, $3::uuid) `, [ids.user, ids.tenantAdminUser, ids.secondStudentUser], ); await client.query( ` insert into public.platform_users (id, auth_user_id, username, phone, name, primary_role, raw_profile) values ($1, $2, 'smoke_tenant_admin', '13800000001', 'Smoke Tenant Admin', 'tenant_admin', '{"source":"smoke-seed"}'::jsonb) on conflict (id) do update set username = excluded.username, auth_user_id = excluded.auth_user_id, phone = excluded.phone, name = excluded.name, primary_role = excluded.primary_role, updated_at = now() `, [ids.tenantAdminUser, ids.authTenantAdminUser], ); await client.query( ` insert into public.platform_users (id, auth_user_id, username, phone, name, primary_role, raw_profile) values ($1, $2, 'smoke_tenant_operator', '13800000003', 'Smoke Tenant Operator', 'tenant_operator', '{"source":"smoke-seed"}'::jsonb), ($3, null, 'smoke_tenant_sales', '13800000004', 'Smoke Tenant Sales', 'sales', '{"source":"smoke-seed"}'::jsonb), ($4, null, 'smoke_tenant_agent', '13800000005', 'Smoke Tenant Agent', 'agent', '{"source":"smoke-seed"}'::jsonb), ($5, null, 'smoke_teacher', '13800000015', 'Smoke Teacher', 'teacher', '{"source":"smoke-seed"}'::jsonb), ($6, null, 'smoke_second_student', '13800000016', 'Smoke Second Student', 'student', '{"source":"smoke-seed"}'::jsonb) on conflict (id) do update set username = excluded.username, auth_user_id = excluded.auth_user_id, phone = excluded.phone, name = excluded.name, primary_role = excluded.primary_role, updated_at = now() `, [ ids.tenantOperatorUser, ids.authTenantOperatorUser, ids.tenantSalesUser, ids.tenantAgentUser, ids.tenantTeacherUser, ids.secondStudentUser, ], ); await client.query( ` insert into public.platform_users (id, auth_user_id, username, phone, name, primary_role, platform_permissions, raw_profile) values ($1, $2, 'smoke_platform_admin', '13999999999', 'Smoke Platform Admin', 'platform_admin', '{"*":true}'::jsonb, '{"source":"smoke-seed"}'::jsonb) on conflict (id) do update set username = excluded.username, auth_user_id = excluded.auth_user_id, phone = excluded.phone, name = excluded.name, primary_role = excluded.primary_role, platform_permissions = excluded.platform_permissions, updated_at = now() `, [ids.platformAdminUser, ids.authPlatformAdminUser], ); await client.query( ` insert into public.platform_users (id, auth_user_id, username, phone, name, primary_role, raw_profile) values ($1, null, 'smoke_partner_admin', '13800000907', 'Smoke Partner 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.partnerTenantAdminUser], ); 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( ` delete from public.tenant_memberships where tenant_id = $1 and user_id = $2 and role = 'tenant_admin' `, [ids.partnerTenant, ids.tenantAdminUser], ); 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.tenant_memberships (tenant_id, user_id, role, status, permissions) values ($1, $2, 'tenant_operator', 'active', '{"marketing:read":true}'::jsonb), ($1, $3, 'platform_admin', 'active', '{"*":true}'::jsonb), ($1, $4, 'teacher', 'active', '{"classes:read":true,"students:read":true}'::jsonb), ($1, $5, 'student', 'active', '{}'::jsonb) on conflict (tenant_id, user_id, role) do update set status = 'active', permissions = excluded.permissions, updated_at = now() `, [tenantId, ids.tenantOperatorUser, ids.platformAdminUser, ids.tenantTeacherUser, ids.secondStudentUser], ); 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.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.secondStudentUser], ); 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( ` update public.student_profiles set region_id = $3, avatar_preset = 'male', last_check_in_date = null, stats = stats - 'checkInStreak' - 'lastCheckInPoints' - 'lastCheckInRewardMode' - 'checkInRewardBreakdown', updated_at = now() where tenant_id = $1 and user_id in ($2::uuid, $4::uuid) `, [tenantId, ids.user, ids.region, ids.secondStudentUser], ); await client.query( ` insert into public.exam_dates ( id, tenant_id, region_id, school_id, legacy_id, exam_name, exam_date, exam_type, description, metadata, sort_order, is_active ) values ( $1, $2, $3, null, 'smoke-exam-date', '烟测统考', '2030-06-15', 'unified', '用于验证学生端考试倒计时和租户后台考试日期维护', '{"source":"smoke-seed"}'::jsonb, 1, true ) on conflict (id) do update set region_id = excluded.region_id, school_id = excluded.school_id, exam_name = excluded.exam_name, exam_date = excluded.exam_date, exam_type = excluded.exam_type, description = excluded.description, metadata = excluded.metadata, sort_order = excluded.sort_order, is_active = excluded.is_active, updated_at = now() `, [ids.examDate, tenantId, ids.region], ); 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, '烟测公共题库', 'platform', 'active', '{"source":"smoke-seed","commercialScope":"public_region_bank"}'::jsonb) on conflict (id) do update set name = excluded.name, region_id = excluded.region_id, source_scope = excluded.source_scope, status = excluded.status, metadata = excluded.metadata, updated_at = now() `, [ids.questionBank, tenantId, ids.region], ); await client.query( ` insert into public.content_entries ( id, tenant_id, region_id, legacy_id, entry_key, name, entry_type, icon, route, description, visibility, layout_config, sort_order, is_active, created_by, updated_by ) values ( $1, $2, $3, 'smoke-practice-entry', 'smoke-question-practice', '烟测刷题入口', 'question_practice', 'book-open', '/practice', '用于验证新内容导航和组卷链路', 'public', '{"tabs":["all","paper","chapter","type"]}'::jsonb, 1, true, $4, $4 ) on conflict (id) do update set region_id = excluded.region_id, name = excluded.name, entry_key = excluded.entry_key, entry_type = excluded.entry_type, layout_config = excluded.layout_config, is_active = true, updated_at = now() `, [ids.contentEntry, tenantId, ids.region, ids.tenantAdminUser], ); await client.query( ` insert into public.content_nodes ( id, tenant_id, entry_id, region_id, parent_id, legacy_id, node_key, name, node_type, marker_type, marker_config, path, depth, sort_order, is_active, is_selectable, is_leaf, metadata, created_by, updated_by ) values ( $1, $2, $5, $6, null, 'smoke-culture-node', 'culture', '文化课', 'category', 'exam_track', '{"intentKey":"culture"}'::jsonb, 'n_000000000000000000000000000000612'::ltree, 0, 1, true, true, true, '{"businessMeaning":"公共课入口"}'::jsonb, $7, $7 ), ( $3, $2, $5, $6, null, 'smoke-professional-node', 'professional', '专业课', 'category', 'exam_track', '{"intentKey":"professional"}'::jsonb, 'n_000000000000000000000000000000613'::ltree, 0, 2, true, true, false, '{"businessMeaning":"院校自主命题入口"}'::jsonb, $7, $7 ), ( $4, $2, $5, $6, $3, 'smoke-school-target-node', 'professional-school-a', '烟测学院专业课', 'school', 'school', '{"schoolName":"烟测学院","salesIntent":true}'::jsonb, 'n_000000000000000000000000000000613.n_000000000000000000000000000000614'::ltree, 1, 1, true, true, true, '{"businessMeaning":"学生目标院校意向"}'::jsonb, $7, $7 ) on conflict (id) do update set name = excluded.name, parent_id = excluded.parent_id, node_type = excluded.node_type, marker_type = excluded.marker_type, marker_config = excluded.marker_config, path = excluded.path, depth = excluded.depth, is_leaf = excluded.is_leaf, metadata = excluded.metadata, updated_at = now() `, [ ids.contentNodeCulture, tenantId, ids.contentNodeProfessional, ids.contentNodeSchoolTarget, ids.contentEntry, ids.region, ids.tenantAdminUser, ], ); await client.query( ` insert into public.question_collections ( id, tenant_id, region_id, entry_id, node_id, subject_id, category_id, question_bank_id, legacy_id, name, collection_type, source_type, filters, question_count, total_score, duration_minutes, status, sort_order, access_rules, metadata, created_by, updated_by ) values ( $1, $2, $3, $4, $5, $6, $7, $8, 'smoke-collection', '烟测学院专业课题目列表', 'manual', 'manual_questions', '{"tabs":["all","paper","chapter","type"]}'::jsonb, 0, 100, 120, 'active', 1, '{"freeDailyLimit":2,"freeSessionLimit":2,"freeQuotaScopeType":"tenant"}'::jsonb, '{"business":"supports sequential random mock exam"}'::jsonb, $9, $9 ) on conflict (id) do update set node_id = excluded.node_id, subject_id = excluded.subject_id, category_id = excluded.category_id, question_bank_id = excluded.question_bank_id, name = excluded.name, collection_type = excluded.collection_type, source_type = excluded.source_type, filters = excluded.filters, total_score = excluded.total_score, duration_minutes = excluded.duration_minutes, status = 'active', access_rules = excluded.access_rules, updated_at = now() `, [ ids.questionCollection, tenantId, ids.region, ids.contentEntry, ids.contentNodeSchoolTarget, ids.subject, ids.category, ids.questionBank, ids.tenantAdminUser, ], ); await client.query( ` insert into public.questions ( id, tenant_id, question_bank_id, subject_id, category_id, entry_id, content_node_id, primary_collection_id, legacy_id, type, type_label, difficulty, status, exam_markers ) values ( $1, $2, $5, $6, $7, $8, $9, $10, 'smoke-question', 'choice', '单选题', 1, 'published', '{"examTrack":"professional","school":"烟测学院"}'::jsonb ), ( $3, $2, $5, $6, $7, $8, $9, $10, 'smoke-question-2', 'choice', '单选题', 1, 'published', '{"examTrack":"professional","school":"烟测学院"}'::jsonb ), ( $4, $2, $5, $6, $7, $8, $9, $10, 'smoke-question-3', 'short_answer', '简答题', 1, 'published', '{"examTrack":"professional","school":"烟测学院"}'::jsonb ) on conflict (id) do update set question_bank_id = excluded.question_bank_id, subject_id = excluded.subject_id, category_id = excluded.category_id, entry_id = excluded.entry_id, content_node_id = excluded.content_node_id, primary_collection_id = excluded.primary_collection_id, type = excluded.type, type_label = excluded.type_label, difficulty = excluded.difficulty, status = excluded.status, exam_markers = excluded.exam_markers, updated_at = now() `, [ ids.question, tenantId, ids.questionTwo, ids.questionThree, ids.questionBank, ids.subject, ids.category, ids.contentEntry, ids.contentNodeSchoolTarget, ids.questionCollection, ], ); await client.query( ` insert into public.question_collection_items ( tenant_id, collection_id, question_id, section_key, sort_order, score, required, metadata ) values ($1, $2, $3, 'choice', 1, 2, true, '{"source":"smoke-seed"}'::jsonb), ($1, $2, $4, 'choice', 2, 2, true, '{"source":"smoke-seed"}'::jsonb), ($1, $2, $5, 'subjective', 3, 2, true, '{"source":"smoke-seed"}'::jsonb) on conflict (tenant_id, collection_id, question_id) do update set section_key = excluded.section_key, sort_order = excluded.sort_order, score = excluded.score, updated_at = now() `, [tenantId, ids.questionCollection, ids.question, ids.questionTwo, ids.questionThree], ); await client.query( ` update public.question_collections set question_count = ( select count(*) from public.question_collection_items where tenant_id = $1 and collection_id = $2 ), updated_at = now() where tenant_id = $1 and id = $2 `, [tenantId, ids.questionCollection], ); await client.query( ` insert into public.practice_blueprints ( id, tenant_id, region_id, entry_id, node_id, collection_id, legacy_id, name, mode, assembly_type, question_limit, duration_minutes, total_score, pass_score, sections, rules, status, sort_order, created_by, updated_by ) values ( $1, $4, $5, $6, $7, $8, 'smoke-sequential-blueprint', '烟测顺序刷题', 'sequential', 'collection', 20, null, null, null, '[]'::jsonb, '{"randomize":false}'::jsonb, 'active', 1, $9, $9 ), ( $2, $4, $5, $6, $7, $8, 'smoke-random-blueprint', '烟测随机刷题', 'random', 'collection', 20, null, null, null, '[]'::jsonb, '{"randomize":true}'::jsonb, 'active', 2, $9, $9 ), ( $3, $4, $5, $6, $7, $8, 'smoke-mock-blueprint', '烟测全真模拟', 'mock_exam', 'collection', 10, 120, 100, 60, '[{"key":"choice","title":"单选题","questionType":"choice","questionCount":2,"scoreEach":2},{"key":"subjective","title":"主观题","questionType":"short_answer","questionCount":1,"scoreEach":2}]'::jsonb, '{"randomize":true,"showAnalysisAfterSubmit":false}'::jsonb, 'active', 3, $9, $9 ) on conflict (id) do update set name = excluded.name, mode = excluded.mode, assembly_type = excluded.assembly_type, question_limit = excluded.question_limit, duration_minutes = excluded.duration_minutes, total_score = excluded.total_score, pass_score = excluded.pass_score, sections = excluded.sections, rules = excluded.rules, status = 'active', updated_at = now() `, [ ids.practiceBlueprintSequential, ids.practiceBlueprintRandom, ids.practiceBlueprintMock, tenantId, ids.region, ids.contentEntry, ids.contentNodeSchoolTarget, ids.questionCollection, ids.tenantAdminUser, ], ); 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, $4, 1, '1 + 1 = ?', '[{"label":"A","text":"1"},{"label":"B","text":"2"},{"label":"C","text":"3"}]'::jsonb, 1, '[1]'::jsonb, '2', '基础加法。' ), ( $3, $2, $5, 1, '2 + 2 = ?', '[{"label":"A","text":"3"},{"label":"B","text":"4"},{"label":"C","text":"5"}]'::jsonb, 1, '[1]'::jsonb, '4', '基础加法。' ), ( $6, $2, $7, 1, '请简述多租户题库为什么必须以后端权限为准。', '[]'::jsonb, null, '[]'::jsonb, '后端需要统一校验租户、角色、资源和订单权益,前端只负责展示体验。', '主观题由学生查看参考答案后自评。' ) 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.questionTwoVersion, ids.question, ids.questionTwo, ids.questionThreeVersion, ids.questionThree], ); await client.query( ` update public.questions q set current_version_id = v.version_id, has_video_explanation = case when q.id = $2::uuid then true else q.has_video_explanation end, updated_at = now() from (values ($2::uuid, $3::uuid), ($4::uuid, $5::uuid), ($6::uuid, $7::uuid) ) as v(question_id, version_id) where q.tenant_id = $1 and q.id = v.question_id `, [ tenantId, ids.question, ids.questionVersion, ids.questionTwo, ids.questionTwoVersion, ids.questionThree, ids.questionThreeVersion, ], ); await client.query( ` insert into public.practice_sessions ( id, tenant_id, user_id, mode, target_type, target_id, blueprint_id, collection_id, entry_id, content_node_id, question_ids, question_count, duration_minutes, total_score, started_at, finished_at, metadata ) values ( $1, $3, $4, 'mock_exam', 'blueprint', $6, $6, $7, $8, $9, jsonb_build_array($10::uuid, $11::uuid), 2, 120, 100, now() - interval '2 days', now() - interval '2 days' + interval '30 minutes', '{"source":"smoke-leaderboard"}'::jsonb ), ( $2, $3, $5, 'mock_exam', 'blueprint', $6, $6, $7, $8, $9, jsonb_build_array($10::uuid, $11::uuid, $12::uuid), 3, 120, 100, now() - interval '1 day', now() - interval '1 day' + interval '35 minutes', '{"source":"smoke-leaderboard"}'::jsonb ) on conflict (id) do update set question_ids = excluded.question_ids, question_count = excluded.question_count, finished_at = excluded.finished_at, metadata = excluded.metadata `, [ ids.leaderboardSessionUser, ids.leaderboardSessionSecond, tenantId, ids.user, ids.secondStudentUser, ids.practiceBlueprintMock, ids.questionCollection, ids.contentEntry, ids.contentNodeSchoolTarget, ids.question, ids.questionTwo, ids.questionThree, ], ); await client.query( ` insert into public.answer_records ( id, tenant_id, user_id, question_id, question_version_id, practice_session_id, selected_options, answer_text, is_correct, answered_at ) values ($1, $5, $6, $8, $9, $12, '["1"]'::jsonb, null, true, now() - interval '2 days'), ($2, $5, $6, $10, $11, $12, '["0"]'::jsonb, null, false, now() - interval '2 days' + interval '1 minute'), ($3, $5, $7, $8, $9, $13, '["1"]'::jsonb, null, true, now() - interval '1 day'), ($4, $5, $7, $10, $11, $13, '["1"]'::jsonb, null, true, now() - interval '1 day' + interval '1 minute') on conflict (id) do update set selected_options = excluded.selected_options, is_correct = excluded.is_correct, answered_at = excluded.answered_at `, [ ids.leaderboardAnswerUserOne, ids.leaderboardAnswerUserTwo, ids.leaderboardAnswerSecondOne, ids.leaderboardAnswerSecondTwo, tenantId, ids.user, ids.secondStudentUser, ids.question, ids.questionVersion, ids.questionTwo, ids.questionTwoVersion, ids.leaderboardSessionUser, ids.leaderboardSessionSecond, ], ); await client.query( ` insert into public.practice_session_reports ( id, tenant_id, user_id, practice_session_id, blueprint_id, collection_id, mode, total_questions, answered_count, correct_count, wrong_count, unanswered_count, score, total_score, accuracy, duration_seconds, started_at, submitted_at, section_stats, question_results, wrong_question_ids, metadata ) values ( $1, $3, $4, $6, $8, $9, 'mock_exam', 2, 2, 1, 1, 0, 70, 100, 0.5, 1800, now() - interval '2 days', now() - interval '2 days' + interval '30 minutes', '[{"key":"choice","title":"单选题","questionCount":2,"correctCount":1,"score":70,"totalScore":100}]'::jsonb, '[]'::jsonb, jsonb_build_array($10::uuid), '{"source":"smoke-leaderboard"}'::jsonb ), ( $2, $3, $5, $7, $8, $9, 'mock_exam', 3, 2, 2, 0, 1, 95, 100, 0.6667, 2100, now() - interval '1 day', now() - interval '1 day' + interval '35 minutes', '[{"key":"choice","title":"单选题","questionCount":3,"correctCount":2,"score":95,"totalScore":100}]'::jsonb, '[]'::jsonb, '[]'::jsonb, '{"source":"smoke-leaderboard"}'::jsonb ) on conflict (tenant_id, practice_session_id) do update set score = excluded.score, total_score = excluded.total_score, accuracy = excluded.accuracy, submitted_at = excluded.submitted_at, section_stats = excluded.section_stats, question_results = excluded.question_results, wrong_question_ids = excluded.wrong_question_ids, metadata = excluded.metadata `, [ ids.leaderboardReportUser, ids.leaderboardReportSecond, tenantId, ids.user, ids.secondStudentUser, ids.leaderboardSessionUser, ids.leaderboardSessionSecond, ids.practiceBlueprintMock, ids.questionCollection, ids.questionTwo, ], ); await client.query( ` insert into public.user_score_events ( id, tenant_id, user_id, event_type, points, balance_after, source_type, idempotency_key, metadata, created_at ) values ( $1, $2, $3, 'activity_reward', 30, 30, 'smoke-leaderboard', 'smoke-leaderboard-score-second', '{"source":"smoke-seed"}'::jsonb, now() - interval '1 day' ) on conflict (id) do update set points = excluded.points, balance_after = excluded.balance_after, metadata = excluded.metadata, created_at = excluded.created_at `, [ids.leaderboardScoreEventSecond, tenantId, ids.secondStudentUser], ); await client.query( ` update public.platform_users set score = case id when $1::uuid then 10 when $2::uuid then 30 else score end, updated_at = now() where id in ($1::uuid, $2::uuid) `, [ids.user, ids.secondStudentUser], ); await client.query( ` update public.student_profiles sp set mastered_words_count = coalesce(word_stats.mastered_count, 0), stats = coalesce(sp.stats, '{}'::jsonb) || jsonb_build_object( 'totalAnswered', coalesce(answer_stats.answer_count, 0), 'correctCount', coalesce(answer_stats.correct_count, 0), 'wrongCount', coalesce(answer_stats.wrong_count, 0) ), updated_at = now() from ( select user_id, count(*)::integer as answer_count, count(*) filter (where is_correct is true)::integer as correct_count, count(*) filter (where is_correct is false)::integer as wrong_count from public.answer_records where tenant_id = $1 and user_id in ($2::uuid, $3::uuid) group by user_id ) answer_stats left join ( select user_id, count(*)::integer as mastered_count from public.user_word_progress where tenant_id = $1 and user_id in ($2::uuid, $3::uuid) and status = 'mastered' group by user_id ) word_stats on word_stats.user_id = answer_stats.user_id where sp.tenant_id = $1 and sp.user_id = answer_stats.user_id `, [tenantId, ids.user, ids.secondStudentUser], ); await client.query( ` insert into public.content_assets ( id, tenant_id, asset_key, title, asset_type, storage_provider, bucket, object_key, file_name, mime_type, file_size_bytes, checksum_sha256, visibility, status, upload_status, verified_at, verified_size_bytes, verified_checksum_sha256, security_scan_status, security_scanned_at, security_scan_provider, security_scan_summary, source ) values ( $1, $2, 'smoke-video-asset', '烟测视频对象', 'video', 'local_dev', 'tenant-assets', $3, 'smoke.mp4', 'video/mp4', 8192, $4, 'svip', 'active', 'verified', now(), 8192, $4, 'passed', now(), 'smoke_seed', '{"riskLevel":"none","issueCodes":[],"provider":"smoke_seed"}'::jsonb, 'smoke-seed' ) on conflict (id) do update set storage_provider = excluded.storage_provider, bucket = excluded.bucket, object_key = excluded.object_key, file_size_bytes = excluded.file_size_bytes, checksum_sha256 = excluded.checksum_sha256, visibility = excluded.visibility, status = 'active', upload_status = 'verified', verified_at = coalesce(public.content_assets.verified_at, now()), verified_size_bytes = excluded.verified_size_bytes, verified_checksum_sha256 = excluded.verified_checksum_sha256, security_scan_status = 'passed', security_scanned_at = coalesce(public.content_assets.security_scanned_at, now()), security_scan_provider = 'smoke_seed', security_scan_summary = excluded.security_scan_summary, updated_at = now() `, [ids.videoAsset, tenantId, `${tenantId}/videos/smoke.mp4`, 'c'.repeat(64)], ); 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, asset_id, access_mode, free_preview_seconds ) 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, $4, 'svip', 15 ) on conflict (id) do update set title = excluded.title, video_url = excluded.video_url, subject_id = excluded.subject_id, asset_id = excluded.asset_id, access_mode = excluded.access_mode, is_active = true, updated_at = now() `, [ids.video, tenantId, ids.subject, ids.videoAsset], ); 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, asset_id, access_mode, free_preview_seconds ) values ( $1, $2, 'smoke-quota-video', '烟测次数视频讲解', '用于验证视频播放次数扣减', 'https://example.test/videos/quota.mp4', 'https://example.test/videos/quota.jpg', 90, '["次数权益","烟测"]'::jsonb, false, $3, 1, 2, true, $4, 'video_quota', 0 ) on conflict (id) do update set title = excluded.title, subject_id = excluded.subject_id, asset_id = excluded.asset_id, access_mode = excluded.access_mode, is_active = true, updated_at = now() `, [ids.quotaVideo, tenantId, ids.subject, ids.videoAsset], ); 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.question_videos ( id, tenant_id, question_id, video_id, legacy_id, video_type, sort_order ) values ($1, $2, $3, $4, 'smoke-quota-question-video', 'quota', 2) on conflict (id) do update set question_id = excluded.question_id, video_id = excluded.video_id, video_type = excluded.video_type, updated_at = now() `, [ids.quotaQuestionVideo, tenantId, ids.question, ids.quotaVideo], ); await client.query( ` insert into public.video_play_quota_accounts ( id, tenant_id, user_id, quota_type, total_quota, used_quota, expires_at, source_type, source_id, metadata ) values ($1, $2, $3, 'video_play', 3, 0, now() + interval '30 days', 'smoke-seed', null, '{"source":"smoke-seed"}'::jsonb) on conflict (id) do update set total_quota = 3, used_quota = 0, expires_at = now() + interval '30 days', updated_at = now() `, [ids.videoQuotaAccount, tenantId, ids.user], ); 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.svip_plans ( id, tenant_id, region_id, legacy_id, name, price_cents, original_price_cents, days, description, per_day_label, badge, recommended, coupon_only, sort_order, is_active ) values ( $1, $2, $3, 'smoke-coupon-plan', '烟测优惠券专享月卡', 1000, 2000, 30, '本地 smoke 优惠券专享套餐', '0.33/天', 'COUPON', false, true, 2, true ) on conflict (id) do update set name = excluded.name, price_cents = excluded.price_cents, original_price_cents = excluded.original_price_cents, days = excluded.days, region_id = excluded.region_id, coupon_only = excluded.coupon_only, is_active = true, updated_at = now() `, [ids.couponOnlyPlan, tenantId, ids.region], ); await client.query( ` insert into public.coupons ( id, tenant_id, legacy_id, code, plan_id, discount_type, discount_value, valid_from, valid_to, max_uses, used_count, source, remark ) values ( $1, $2, 'smoke-coupon', 'SMOKE50', $3, 'percent', 50, now() - interval '1 day', now() + interval '30 days', 100, 0, 'smoke', '本地 smoke 优惠券' ), ( $4, $2, 'smoke-free-coupon', 'SMOKEFREE', $3, 'percent', 100, now() - interval '1 day', now() + interval '30 days', 100, 0, 'smoke', '本地 smoke 全额优惠券' ) on conflict (id) do update set code = excluded.code, plan_id = excluded.plan_id, discount_type = excluded.discount_type, discount_value = excluded.discount_value, valid_from = excluded.valid_from, valid_to = excluded.valid_to, max_uses = excluded.max_uses, used_count = 0, source = excluded.source, remark = excluded.remark, updated_at = now() `, [ids.checkoutCoupon, tenantId, ids.couponOnlyPlan, ids.freeCheckoutCoupon], ); 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.activation_codes ( id, tenant_id, legacy_id, code, days, is_used, agent_user_id, sale_type, unit_price_cents, used_region_id, remark ) values ($1, $2, 'smoke-self-code', 'SMOKESELF20260621', 30, false, $3, 'sales', 0, $4, '不可自用的 smoke 激活码') on conflict (id) do update set is_used = false, used_by = null, used_at = null, agent_user_id = excluded.agent_user_id, used_region_id = excluded.used_region_id, updated_at = now() `, [ids.selfActivationCode, tenantId, ids.user, 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 背单词单元', 2, 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 ), ( $4, $2, $3, 'smoke-word-2', 'benefit', '/ˈbenɪfɪt/', '好处', 'Practice brings benefit.', '练习会带来好处。', 1, '["smoke","basic"]'::jsonb, 2, 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, ids.vocabularyWordTwo], ); await client.query( ` insert into public.user_word_progress ( tenant_id, user_id, word_id, status, correct_count, wrong_count, review_count, correct_streak, last_result, due_level, last_review_date, next_review_date ) values ($1, $2, $3, 'learning', 1, 0, 1, 1, 'known', 'soon', now(), now() + interval '1 day'), ($1, $4, $3, 'mastered', 3, 0, 3, 3, 'known', 'mastered', now(), now() + interval '7 days'), ($1, $4, $5, 'mastered', 3, 0, 3, 3, 'known', 'mastered', now(), now() + interval '7 days') on conflict (tenant_id, user_id, word_id) do update set status = excluded.status, correct_count = excluded.correct_count, wrong_count = excluded.wrong_count, review_count = excluded.review_count, correct_streak = excluded.correct_streak, last_result = excluded.last_result, due_level = excluded.due_level, last_review_date = excluded.last_review_date, next_review_date = excluded.next_review_date, updated_at = now() `, [tenantId, ids.user, ids.vocabularyWord, ids.secondStudentUser, ids.vocabularyWordTwo], ); 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.tenant_classes ( id, tenant_id, region_id, legacy_id, code, name, description, status, sort_order, metadata, created_by, updated_by ) values ($1, $3, $4, 'smoke-class-main', 'smoke-main', '烟测主班级', '用于教师学生范围权限测试', 'active', 1, '{"source":"smoke-seed"}'::jsonb, $5, $5), ($2, $3, $4, 'smoke-class-other', 'smoke-other', '烟测其他班级', '用于验证非负责班级不可见', 'active', 2, '{"source":"smoke-seed"}'::jsonb, $5, $5) on conflict (id) do update set region_id = excluded.region_id, code = excluded.code, name = excluded.name, description = excluded.description, status = excluded.status, sort_order = excluded.sort_order, metadata = excluded.metadata, updated_by = excluded.updated_by, updated_at = now() `, [ids.tenantClass, ids.tenantClassOther, tenantId, ids.region, ids.tenantAdminUser], ); await client.query( ` insert into public.tenant_class_members ( tenant_id, class_id, user_id, member_type, status, metadata, created_by, updated_by ) values ($1, $2, $4, 'teacher', 'active', '{"source":"smoke-seed"}'::jsonb, $6, $6), ($1, $2, $5, 'student', 'active', '{"source":"smoke-seed"}'::jsonb, $6, $6), ($1, $3, $7, 'student', 'active', '{"source":"smoke-seed"}'::jsonb, $6, $6) on conflict (tenant_id, class_id, user_id, member_type) do update set status = excluded.status, metadata = excluded.metadata, updated_by = excluded.updated_by, updated_at = now() `, [ tenantId, ids.tenantClass, ids.tenantClassOther, ids.tenantTeacherUser, ids.user, ids.tenantAdminUser, ids.secondStudentUser, ], ); 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, jsonb_build_object( 'source', 'smoke-seed', 'publicQuestionBankAccess', jsonb_build_object( 'mode', 'limited_regions', 'allowedRegionIds', jsonb_build_array($3::text) ) ) ) on conflict (id) do update set plan_code = excluded.plan_code, status = excluded.status, starts_at = excluded.starts_at, expires_at = excluded.expires_at, billing_cycle = excluded.billing_cycle, amount_cents = excluded.amount_cents, metadata = excluded.metadata, updated_at = now() `, [ids.partnerSubscription, ids.partnerTenant, ids.region], ); 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( ` 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() `, [ids.partnerTenant, ids.partnerTenantAdminUser], ); await client.query( ` insert into public.question_bank_grants ( id, source_question_bank_id, grant_scope, allowed_plan_codes, allowed_tenant_ids, status, starts_at, expires_at, metadata ) values ( $1, $2, 'plans', array['starter_yearly','pro_yearly']::text[], '{}'::uuid[], 'active', '2026-06-21T00:00:00Z', null, '{"source":"smoke-seed","business":"public_question_bank_authorization"}'::jsonb ) on conflict (id) do update set grant_scope = excluded.grant_scope, allowed_plan_codes = excluded.allowed_plan_codes, status = excluded.status, starts_at = excluded.starts_at, expires_at = excluded.expires_at, metadata = excluded.metadata, updated_at = now() `, [ids.publicQuestionBankGrant, ids.questionBank], ); await client.query( ` delete from public.tenant_question_bank_adoptions where tenant_id = $1 and source_question_bank_id = $2 `, [ids.partnerTenant, ids.questionBank], ); await client.query( ` delete from public.question_collection_items where tenant_id = $1 and metadata->>'source' = 'public_question_bank_adoption' `, [ids.partnerTenant], ); await client.query( ` delete from public.question_versions where tenant_id = $1 and question_id in ( select id from public.questions where tenant_id = $1 and legacy_id like 'public:%' ) `, [ids.partnerTenant], ); await client.query( ` delete from public.questions where tenant_id = $1 and legacy_id like 'public:%' `, [ids.partnerTenant], ); await client.query( ` delete from public.question_collections where tenant_id = $1 and metadata->>'source' = 'public_question_bank_adoption' `, [ids.partnerTenant], ); await client.query( ` delete from public.content_entries where tenant_id = $1 and entry_key like 'public-%' `, [ids.partnerTenant], ); await client.query( ` delete from public.question_banks where tenant_id = $1 and metadata->>'source' = 'public_question_bank_adoption' `, [ids.partnerTenant], ); 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; });