Files
gongxue-base/scripts/smoke-seed.js

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