From fe9428f36c358a2dcf430b61dff523f898d59f48 Mon Sep 17 00:00:00 2001 From: William Tso Date: Mon, 10 Mar 2025 20:27:12 +0800 Subject: [PATCH] add test data --- backend/package.json | 12 +- backend/scripts/analytics-test.ts | 301 ++++++++++++++++ backend/scripts/generate-fake-data.ts | 483 ++++++++++++++++++++++++++ 3 files changed, 794 insertions(+), 2 deletions(-) create mode 100644 backend/scripts/analytics-test.ts create mode 100644 backend/scripts/generate-fake-data.ts diff --git a/backend/package.json b/backend/package.json index 7b99cac..f8e2977 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,8 @@ "build": "tsc", "start": "node dist/index.js", "lint": "eslint src --ext .ts", - "test": "vitest run" + "test": "vitest run", + "test:analytics": "tsx scripts/analytics-test.ts" }, "keywords": [], "author": "", @@ -26,13 +27,20 @@ "redis": "^4.7.0" }, "devDependencies": { + "@clickhouse/client": "^1.10.1", + "@types/axios": "^0.14.4", + "@types/dotenv": "^8.2.3", "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.11.30", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/parser": "^7.4.0", + "axios": "^1.8.2", + "dotenv": "^16.4.7", "eslint": "^8.57.0", "tsx": "^4.7.1", "typescript": "^5.4.3", + "uuid": "^11.1.0", "vitest": "^1.4.0" } -} \ No newline at end of file +} diff --git a/backend/scripts/analytics-test.ts b/backend/scripts/analytics-test.ts new file mode 100644 index 0000000..2daad4a --- /dev/null +++ b/backend/scripts/analytics-test.ts @@ -0,0 +1,301 @@ +import axios from 'axios'; +import dotenv from 'dotenv'; +import { v4 as uuidv4 } from 'uuid'; + +// 加载 .env 文件 +dotenv.config(); + +const API_URL = `http://localhost:${process.env.PORT || 4000}/api`; +let authToken: string; + +// 测试数据 +const testData = { + influencerId: '20000000-0000-0000-0000-000000000003', + postId: '30000000-0000-0000-0000-000000000006', + projectId: '10000000-0000-0000-0000-000000000001' +}; + +// 测试账号信息 +const testAccount = { + email: 'vitalitymailg@gmail.com', + password: 'password123' +}; + +// 设置请求客户端 +const apiClient = axios.create({ + baseURL: API_URL, + timeout: 10000, + validateStatus: () => true // 不抛出HTTP错误 +}); + +// 设置请求拦截器添加令牌 +apiClient.interceptors.request.use(config => { + if (authToken) { + config.headers.Authorization = `Bearer ${authToken}`; + } + return config; +}); + +// 登录并获取令牌 +async function login() { + try { + console.log('🔑 登录获取认证令牌...'); + console.log(`使用账号: ${testAccount.email}`); + + const response = await apiClient.post('/auth/login', { + email: testAccount.email, + password: testAccount.password + }); + + console.log(`登录响应状态码: ${response.status}`); + + // 根据实际响应结构调整获取token的逻辑 + if (response.status !== 200) { + throw new Error(`登录失败: ${JSON.stringify(response.data)}`); + } + + // 默认假设响应中包含 access_token 或 token 字段 + if (response.data.access_token) { + authToken = response.data.access_token; + } else if (response.data.token) { + authToken = response.data.token; + } else if (response.data.data?.session?.access_token) { + // Supabase 可能返回嵌套在 data.session 中的 token + authToken = response.data.data.session.access_token; + } else { + console.log('警告: 响应中没有找到明确的 token 字段,尝试直接使用响应数据...'); + console.log('响应结构:', JSON.stringify(response.data, null, 2)); + + // 如果找不到标准字段,则尝试其他可能的位置 + authToken = response.data; + } + + if (!authToken) { + throw new Error('未能从响应中提取认证令牌'); + } + + console.log('✅ 登录成功,获取到令牌'); + return true; + } catch (error) { + console.error('❌ 登录失败:', error); + return false; + } +} + +// 测试网红指标追踪接口 +async function testInfluencerTrack() { + try { + console.log('\n🧪 测试网红指标追踪接口...'); + const response = await apiClient.post('/analytics/influencer/track', { + influencer_id: testData.influencerId, + metrics: { + followers_count: 50000, + video_count: 120, + views_count: 1500000, + likes_count: 300000 + } + }); + + console.log(`状态码: ${response.status}`); + console.log('响应数据:', JSON.stringify(response.data, null, 2)); + + return response.status === 200; + } catch (error) { + console.error('测试失败:', error); + return false; + } +} + +// 测试内容指标追踪接口 +async function testContentTrack() { + try { + console.log('\n🧪 测试内容指标追踪接口...'); + const response = await apiClient.post('/analytics/content/track', { + post_id: testData.postId, + metrics: { + views_count: 5000, + likes_count: 500, + comments_count: 50, + shares_count: 20 + } + }); + + console.log(`状态码: ${response.status}`); + console.log('响应数据:', JSON.stringify(response.data, null, 2)); + + return response.status === 200; + } catch (error) { + console.error('测试失败:', error); + return false; + } +} + +// 测试网红增长趋势接口 +async function testInfluencerGrowth() { + try { + console.log('\n🧪 测试网红增长趋势接口...'); + const response = await apiClient.get(`/analytics/influencer/${testData.influencerId}/growth`, { + params: { + metric: 'followers_count', + timeframe: '6months', + interval: 'month' + } + }); + + console.log(`状态码: ${response.status}`); + console.log('响应数据样例 (仅显示部分):', JSON.stringify({ + ...response.data, + data: response.data.data?.slice(0, 2) || [] + }, null, 2)); + + return response.status === 200; + } catch (error) { + console.error('测试失败:', error); + return false; + } +} + +// 测试内容互动趋势接口 +async function testContentTrends() { + try { + console.log('\n🧪 测试内容互动趋势接口...'); + const response = await apiClient.get(`/analytics/content/${testData.postId}/trends`, { + params: { + metric: 'views_count', + timeframe: '30days', + interval: 'day' + } + }); + + console.log(`状态码: ${response.status}`); + console.log('响应数据样例 (仅显示部分):', JSON.stringify({ + ...response.data, + data: response.data.data?.slice(0, 2) || [] + }, null, 2)); + + return response.status === 200; + } catch (error) { + console.error('测试失败:', error); + return false; + } +} + +// 测试项目概览接口 +async function testProjectOverview() { + try { + console.log('\n🧪 测试项目概览接口...'); + const response = await apiClient.get(`/analytics/project/${testData.projectId}/overview`, { + params: { + timeframe: '30days' + } + }); + + console.log(`状态码: ${response.status}`); + console.log('响应数据样例 (仅显示部分):', JSON.stringify({ + project: response.data.project, + timeframe: response.data.timeframe, + metrics: response.data.metrics, + platforms: response.data.platforms, + timeline: response.data.timeline?.slice(0, 2) || [] + }, null, 2)); + + return response.status === 200; + } catch (error) { + console.error('测试失败:', error); + return false; + } +} + +// 测试项目表现最佳网红接口 +async function testProjectTopPerformers() { + try { + console.log('\n🧪 测试项目表现最佳网红接口...'); + const response = await apiClient.get(`/analytics/project/${testData.projectId}/top-performers`, { + params: { + metric: 'views_count', + limit: '5', + timeframe: '30days' + } + }); + + console.log(`状态码: ${response.status}`); + console.log('响应数据样例 (仅显示部分):', JSON.stringify({ + project_id: response.data.project_id, + metric: response.data.metric, + timeframe: response.data.timeframe, + top_performers: response.data.top_performers?.slice(0, 2) || [] + }, null, 2)); + + return response.status === 200; + } catch (error) { + console.error('测试失败:', error); + return false; + } +} + +// 测试项目报告导出接口 +async function testProjectReport() { + try { + console.log('\n🧪 测试项目报告接口...'); + const response = await apiClient.get(`/analytics/reports/project/${testData.projectId}`, { + params: { + timeframe: '30days', + format: 'json' + } + }); + + console.log(`状态码: ${response.status}`); + console.log('响应数据样例 (仅显示部分):', JSON.stringify({ + report_type: response.data.report_type, + generated_at: response.data.generated_at, + project: response.data.project, + summary: response.data.summary + }, null, 2)); + + return response.status === 200; + } catch (error) { + console.error('测试失败:', error); + return false; + } +} + +// 运行所有测试 +async function runTests() { + console.log('🚀 开始 Analytics 接口测试...\n'); + + // 获取认证令牌 + const loginSuccess = await login(); + if (!loginSuccess) { + console.error('由于登录失败,测试中止'); + process.exit(1); + } + + // 运行测试用例 + const results = { + influencerTrack: await testInfluencerTrack(), + contentTrack: await testContentTrack(), + influencerGrowth: await testInfluencerGrowth(), + contentTrends: await testContentTrends(), + projectOverview: await testProjectOverview(), + projectTopPerformers: await testProjectTopPerformers(), + projectReport: await testProjectReport() + }; + + // 显示测试结果摘要 + console.log('\n📊 测试结果摘要:'); + + for (const [test, passed] of Object.entries(results)) { + console.log(`${passed ? '✅' : '❌'} ${test}`); + } + + const totalTests = Object.keys(results).length; + const passedTests = Object.values(results).filter(Boolean).length; + + console.log(`\n总结: ${passedTests}/${totalTests} 测试通过`); +} + +// 执行测试 +runTests().catch(error => { + console.error('测试过程中发生错误:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/backend/scripts/generate-fake-data.ts b/backend/scripts/generate-fake-data.ts new file mode 100644 index 0000000..624fa25 --- /dev/null +++ b/backend/scripts/generate-fake-data.ts @@ -0,0 +1,483 @@ +import axios from 'axios'; +import dotenv from 'dotenv'; +import { v4 as uuidv4 } from 'uuid'; +import { createClient } from '@clickhouse/client'; + +// 加载环境变量 +dotenv.config(); + +// 配置参数 +const API_URL = `http://localhost:${process.env.PORT || 4000}/api`; +let AUTH_TOKEN = ''; // 登录后填入有权限的认证令牌 +const CLICKHOUSE_HOST = process.env.CLICKHOUSE_HOST || 'localhost'; +const CLICKHOUSE_PORT = process.env.CLICKHOUSE_PORT || '8123'; +const CLICKHOUSE_USER = process.env.CLICKHOUSE_USER || 'admin'; +const CLICKHOUSE_PASSWORD = process.env.CLICKHOUSE_PASSWORD || 'your_secure_password'; +const CLICKHOUSE_DATABASE = process.env.CLICKHOUSE_DATABASE || 'promote'; + +// 设置时间范围 - 生成过去 1 年数据 +const END_DATE = new Date(); +const START_DATE = new Date(); +START_DATE.setFullYear(END_DATE.getFullYear() - 1); // 1年前 + +// 测试账号 +const TEST_ACCOUNT = { + email: 'vitalitymailg@gmail.com', + password: 'password123' +}; + +// 测试数据 - 可以根据需要修改 +const TEST_DATA = { + influencerIds: [ + '20000000-0000-0000-0000-000000000001', + '20000000-0000-0000-0000-000000000002', + '20000000-0000-0000-0000-000000000003', + '20000000-0000-0000-0000-000000000004', + '20000000-0000-0000-0000-000000000005' + ], + projectIds: [ + '10000000-0000-0000-0000-000000000001', + '10000000-0000-0000-0000-000000000002' + ], + platformList: ['youtube', 'instagram', 'tiktok', 'twitter', 'facebook'] +}; + +// 帖子内容模板 +const POST_TEMPLATES = [ + { title: '产品介绍视频', description: '展示产品功能和优势' }, + { title: '使用教程', description: '详细介绍如何使用产品的功能' }, + { title: '用户案例分享', description: '真实用户使用产品的体验分享' }, + { title: '行业趋势解析', description: '分析当前行业发展趋势和机遇' }, + { title: '产品评测', description: '全面评测产品的性能和体验' }, + { title: '问答视频', description: '解答用户常见问题' }, + { title: '活动宣传', description: '推广最新的营销活动和优惠' }, + { title: '幕后花絮', description: '分享产品研发和团队的故事' }, + { title: '用户反馈总结', description: '汇总用户反馈并分享改进计划' }, + { title: '行业对比分析', description: '与竞品的对比分析' } +]; + +// 评论模板 +const COMMENT_TEMPLATES = [ + '这个产品太棒了!我已经使用了一个月,效果非常好。', + '请问这个产品适合初学者使用吗?', + '价格有点高,但质量确实不错。', + '我有一个问题,能否详细说明一下第三个功能怎么用?', + '已经购买并且推荐给朋友了,大家都很满意。', + '比我之前用的产品好用多了,界面设计也很友好。', + '发货速度快,客服态度好,产品质量一流!', + '还在考虑要不要买,有没有更多实际使用的案例?', + '刚收到货,包装很精美,期待使用效果。', + '产品有保修吗?保修期是多久?' +]; + +// 设置请求客户端 +const apiClient = axios.create({ + baseURL: API_URL, + timeout: 10000, + validateStatus: () => true // 不抛出HTTP错误 +}); + +// 创建 ClickHouse 客户端 +const clickhouse = createClient({ + url: `http://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}`, + username: CLICKHOUSE_USER, + password: CLICKHOUSE_PASSWORD, + database: CLICKHOUSE_DATABASE, +}); + +// 设置请求拦截器添加令牌 +apiClient.interceptors.request.use(config => { + if (AUTH_TOKEN) { + config.headers.Authorization = `Bearer ${AUTH_TOKEN}`; + } + return config; +}); + +// 生成随机数 +function getRandomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +// 生成随机日期 +function getRandomDate(start: Date, end: Date): Date { + return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); +} + +// 获取认证令牌 +async function login(): Promise { + try { + console.log('🔑 登录获取认证令牌...'); + const response = await apiClient.post('/auth/login', { + email: TEST_ACCOUNT.email, + password: TEST_ACCOUNT.password + }); + + console.log(`登录响应状态码: ${response.status}`); + + if (response.status !== 200) { + throw new Error(`登录失败: ${JSON.stringify(response.data)}`); + } + + let token = ''; + + // 尝试从多个可能的位置获取令牌 + if (response.data.access_token) { + token = response.data.access_token; + } else if (response.data.token) { + token = response.data.token; + } else if (response.data.data?.session?.access_token) { + token = response.data.data.session.access_token; + } else { + console.log('警告: 未找到标准令牌格式,使用整个响应作为令牌'); + token = response.data; + } + + console.log('✅ 登录成功'); + return token; + } catch (error) { + console.error('❌ 登录失败:', error); + return ''; + } +} + +// 创建多个假帖子 +async function createFakePosts(count: number): Promise { + console.log(`\n📝 开始创建 ${count} 个假帖子...`); + const postIds: string[] = []; + + for (let i = 0; i < count; i++) { + try { + const template = POST_TEMPLATES[Math.floor(Math.random() * POST_TEMPLATES.length)]; + const influencerId = TEST_DATA.influencerIds[Math.floor(Math.random() * TEST_DATA.influencerIds.length)]; + const projectId = TEST_DATA.projectIds[Math.floor(Math.random() * TEST_DATA.projectIds.length)]; + const platform = TEST_DATA.platformList[Math.floor(Math.random() * TEST_DATA.platformList.length)]; + + // 生成随机发布日期(过去一年内) + const publishDate = getRandomDate(START_DATE, END_DATE); + + // 为不同平台生成不同格式的URL + let postUrl = ''; + if (platform === 'youtube') { + postUrl = `https://youtube.com/watch?v=${uuidv4().substring(0, 8)}`; + } else if (platform === 'instagram') { + postUrl = `https://instagram.com/p/${generateRandomString(10)}`; + } else if (platform === 'tiktok') { + postUrl = `https://tiktok.com/@user/video/${Math.floor(Math.random() * 10000000000)}`; + } else if (platform === 'twitter') { + postUrl = `https://twitter.com/user/status/${Math.floor(Math.random() * 10000000000)}`; + } else if (platform === 'facebook') { + postUrl = `https://facebook.com/user/posts/${uuidv4().substring(0, 10)}`; + } else { + postUrl = `https://example.com/post/${uuidv4()}`; + } + + const postData = { + title: `${template.title} #${i + 1}`, + description: `${template.description} - 自动生成的测试内容 ${i + 1}`, + content: `这是一个自动生成的测试帖子内容。这是帖子 #${i + 1},包含有关产品的详细信息和营销内容。`, + influencer_id: influencerId, + project_id: projectId, + platform, + post_url: postUrl, + status: 'published', + published_at: publishDate.toISOString() + }; + + const response = await apiClient.post('/posts', postData); + + if (response.status === 201 || response.status === 200) { + // 打印完整响应以便调试 + console.log(`帖子 #${i + 1} 响应:`, JSON.stringify(response.data, null, 2)); + + // 尝试从不同位置提取ID + let postId = null; + if (response.data && response.data.post && response.data.post.post_id) { + postId = response.data.post.post_id; + } else if (response.data && response.data.id) { + postId = response.data.id; + } else if (response.data && response.data.post && response.data.post.id) { + postId = response.data.post.id; + } else if (response.data && response.data._id) { + postId = response.data._id; + } else if (response.data && response.data.postId) { + postId = response.data.postId; + } + + if (postId) { + postIds.push(postId); + console.log(`✅ 帖子 #${i + 1} 创建成功 (ID: ${postId})`); + } else { + console.warn(`⚠️ 帖子 #${i + 1} 创建成功但无法提取ID,将无法为此帖子创建评论`); + } + } else { + console.error(`❌ 帖子 #${i + 1} 创建失败:`, response.data); + } + } catch (error) { + console.error(`❌ 创建帖子 #${i + 1} 时出错:`, error); + } + + // 添加随机延迟以避免请求过于频繁 + await new Promise(resolve => setTimeout(resolve, getRandomInt(100, 300))); + } + + console.log(`📊 总计创建了 ${postIds.length}/${count} 个帖子`); + return postIds; +} + +// 生成随机字符串 +function generateRandomString(length: number): string { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + + return result; +} + +// 为帖子创建假评论 +async function createFakeComments(postIds: string[], commentsPerPost: number): Promise { + console.log(`\n💬 开始为 ${postIds.length} 个帖子创建评论 (每个帖子 ${commentsPerPost} 条)...`); + let totalCreated = 0; + + for (const postId of postIds) { + for (let i = 0; i < commentsPerPost; i++) { + try { + const commentTemplate = COMMENT_TEMPLATES[Math.floor(Math.random() * COMMENT_TEMPLATES.length)]; + const commentDate = getRandomDate(START_DATE, END_DATE); + + const commentData = { + content: `${commentTemplate} #${i + 1}`, + user_id: uuidv4(), // 使用随机用户ID + created_at: commentDate.toISOString() + }; + + // 打印请求数据用于调试 + console.log(`尝试为帖子 ${postId} 创建评论:`, commentData); + + const response = await apiClient.post(`/posts/${postId}/comments`, commentData); + + if (response.status === 201 || response.status === 200) { + totalCreated++; + console.log(`✅ 为帖子 ${postId} 创建评论 #${i + 1} 成功`); + if (totalCreated % 10 === 0) { + console.log(`📊 已创建 ${totalCreated} 条评论`); + } + } else { + console.error(`❌ 为帖子 ${postId} 创建评论 #${i + 1} 失败:`, response.data); + } + } catch (error) { + console.error(`❌ 创建评论时出错:`, error); + } + + // 添加随机延迟 + await new Promise(resolve => setTimeout(resolve, getRandomInt(50, 200))); + } + } + + console.log(`📊 总计创建了 ${totalCreated} 条评论`); +} + +// 直接向 ClickHouse 插入分析数据 +async function generateAnalyticsData(postIds: string[]): Promise { + console.log('\n📈 开始生成分析数据...'); + + try { + // 生成视图事件数据 + console.log('生成视图事件数据...'); + interface ViewEvent { + user_id: string; + content_id: string; + timestamp: string; + ip: string; + user_agent: string; + } + const viewEvents: ViewEvent[] = []; + + // 为每个帖子生成多条视图记录,跨越一年时间 + for (const postId of postIds) { + // 每个帖子生成50-200条视图事件 + const viewCount = getRandomInt(50, 200); + + for (let i = 0; i < viewCount; i++) { + const viewDate = getRandomDate(START_DATE, END_DATE); + const userId = uuidv4(); // 模拟不同用户 + const ip = `192.168.${getRandomInt(1, 255)}.${getRandomInt(1, 255)}`; + const userAgents = [ + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15', + 'Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0' + ]; + const userAgent = userAgents[Math.floor(Math.random() * userAgents.length)]; + + viewEvents.push({ + user_id: userId, + content_id: postId, + timestamp: viewDate.toISOString(), + ip: ip, + user_agent: userAgent + }); + } + } + + // 批量插入视图事件 + if (viewEvents.length > 0) { + try { + for (let i = 0; i < viewEvents.length; i += 1000) { // 每批最多1000条 + const batch = viewEvents.slice(i, i + 1000); + await clickhouse.insert({ + table: 'view_events', + values: batch, + format: 'JSONEachRow' + }); + console.log(`✅ 已插入 ${Math.min(i + 1000, viewEvents.length)}/${viewEvents.length} 条视图事件`); + } + console.log('✅ 视图事件数据生成完成'); + } catch (error) { + console.error('插入视图事件失败:', error); + } + } + + // 生成点赞事件数据 + console.log('生成点赞事件数据...'); + interface LikeEvent { + user_id: string; + content_id: string; + timestamp: string; + action: string; + } + const likeEvents: LikeEvent[] = []; + + for (const postId of postIds) { + // 每个帖子生成10-50条点赞事件 + const likeCount = getRandomInt(10, 50); + + for (let i = 0; i < likeCount; i++) { + const likeDate = getRandomDate(START_DATE, END_DATE); + const userId = uuidv4(); + const action = Math.random() > 0.1 ? 'like' : 'unlike'; // 90% 是点赞,10% 是取消点赞 + + likeEvents.push({ + user_id: userId, + content_id: postId, + timestamp: likeDate.toISOString(), + action: action + }); + } + } + + // 批量插入点赞事件 + if (likeEvents.length > 0) { + try { + for (let i = 0; i < likeEvents.length; i += 1000) { // 每批最多1000条 + const batch = likeEvents.slice(i, i + 1000); + await clickhouse.insert({ + table: 'like_events', + values: batch, + format: 'JSONEachRow' + }); + console.log(`✅ 已插入 ${Math.min(i + 1000, likeEvents.length)}/${likeEvents.length} 条点赞事件`); + } + console.log('✅ 点赞事件数据生成完成'); + } catch (error) { + console.error('插入点赞事件失败:', error); + } + } + + // 生成关注事件数据 + console.log('生成关注事件数据...'); + interface FollowerEvent { + follower_id: string; + followed_id: string; + timestamp: string; + action: string; + } + const followerEvents: FollowerEvent[] = []; + + // 为每个网红生成50-200个关注者 + for (const influencerId of TEST_DATA.influencerIds) { + const followerCount = getRandomInt(50, 200); + + for (let i = 0; i < followerCount; i++) { + const followDate = getRandomDate(START_DATE, END_DATE); + const followerId = uuidv4(); + const action = Math.random() > 0.2 ? 'follow' : 'unfollow'; // 80% 是关注,20% 是取消关注 + + followerEvents.push({ + follower_id: followerId, + followed_id: influencerId, + timestamp: followDate.toISOString(), + action: action + }); + } + } + + // 批量插入关注事件 + if (followerEvents.length > 0) { + try { + for (let i = 0; i < followerEvents.length; i += 1000) { // 每批最多1000条 + const batch = followerEvents.slice(i, i + 1000); + await clickhouse.insert({ + table: 'follower_events', + values: batch, + format: 'JSONEachRow' + }); + console.log(`✅ 已插入 ${Math.min(i + 1000, followerEvents.length)}/${followerEvents.length} 条关注事件`); + } + console.log('✅ 关注事件数据生成完成'); + } catch (error) { + console.error('插入关注事件失败:', error); + } + } + + console.log('📊 所有分析数据生成完成!'); + } catch (error) { + console.error('❌ 生成分析数据时出错:', error); + } +} + +// 主函数 +async function main() { + console.log('🚀 开始生成假数据'); + + // 1. 登录获取认证令牌 + const token = await login(); + if (!token) { + console.error('无法获取认证令牌,终止操作'); + process.exit(1); + } + + // 更新全局认证令牌 + AUTH_TOKEN = token; + apiClient.interceptors.request.use(config => { + config.headers.Authorization = `Bearer ${AUTH_TOKEN}`; + return config; + }); + + // 2. 创建假帖子 + const postCount = 20; // 要创建的帖子数量 + const postIds = await createFakePosts(postCount); + + if (postIds.length === 0) { + console.warn('没有成功创建帖子,跳过后续步骤'); + process.exit(1); + } + + // 3. 为帖子创建假评论 + const commentsPerPost = 5; // 每个帖子的评论数量 + await createFakeComments(postIds, commentsPerPost); + + // 4. 生成分析数据 + await generateAnalyticsData(postIds); + + console.log('\n🎉 数据生成完成!'); + process.exit(0); +} + +// 执行主函数 +main().catch(error => { + console.error('程序执行出错:', error); + process.exit(1); +}); \ No newline at end of file