Files
gongxue-base/scripts/smoke-seed.js
2026-06-30 07:50:02 +08:00

2332 lines
82 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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',
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],
],
);
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]],
);
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],
[
`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}`,
],
],
);
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]],
);
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,
last_check_in_date = null,
stats = stats - 'checkInStreak' - 'lastCheckInPoints',
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, '{"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(
`
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;
});