diff --git a/backend/.env b/backend/.env index 717aac0..f3bf938 100644 --- a/backend/.env +++ b/backend/.env @@ -3,7 +3,7 @@ PORT=4000 SUPABASE_URL="https://xtqhluzornazlmkonucr.supabase.co" SUPABASE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0cWhsdXpvcm5hemxta29udWNyIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTI0NDcxMywiZXhwIjoyMDU2ODIwNzEzfQ.E9lHlmdoqCZ9zYg0ammuW6ua-__tEEATCrwYv3-Th3I" SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0cWhsdXpvcm5hemxta29udWNyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEyNDQ3MTMsImV4cCI6MjA1NjgyMDcxM30.PUXbyHOgkvFE6T5fvCFTV7LJq-MbMkRtNdw2VoKJOAg" -DATABASE_URL="postgresql://postgres:KR$kH9fdwZAd@tdS@db.xtqhluzornazlmkonucr.supabase.co:5432/postgres" +DATABASE_URL="postgresql://postgres.xtqhluzornazlmkonucr:KR$kH9fdwZAd@tdS@aws-0-ap-southeast-1.pooler.supabase.com:5432/postgres" REDIS_HOST="localhost" REDIS_PORT="6379" diff --git a/backend/package.json b/backend/package.json index 080624f..c42d2eb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -46,4 +46,4 @@ "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 deleted file mode 100644 index 2daad4a..0000000 --- a/backend/scripts/analytics-test.ts +++ /dev/null @@ -1,301 +0,0 @@ -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/check-clickhouse-data.js b/backend/scripts/check-clickhouse-data.js deleted file mode 100644 index cdfab62..0000000 --- a/backend/scripts/check-clickhouse-data.js +++ /dev/null @@ -1,177 +0,0 @@ -require('dotenv').config(); -const { createClient } = require('@clickhouse/client'); -const http = require('http'); - -// 创建ClickHouse客户端 -const client = createClient({ - host: `http://${process.env.CLICKHOUSE_HOST || 'localhost'}:${process.env.CLICKHOUSE_PORT || 8123}`, - username: process.env.CLICKHOUSE_USER || 'default', - password: process.env.CLICKHOUSE_PASSWORD || '', - database: process.env.CLICKHOUSE_DATABASE || 'promote', -}); - -// 使用HTTP直接发送请求到ClickHouse -function sendClickHouseQuery(query) { - return new Promise((resolve, reject) => { - // 添加认证信息 - const username = process.env.CLICKHOUSE_USER || 'default'; - const password = process.env.CLICKHOUSE_PASSWORD || ''; - const auth = Buffer.from(`${username}:${password}`).toString('base64'); - - const options = { - hostname: process.env.CLICKHOUSE_HOST || 'localhost', - port: process.env.CLICKHOUSE_PORT || 8123, - path: `/?database=${process.env.CLICKHOUSE_DATABASE || 'promote'}&enable_http_compression=1`, - method: 'POST', - headers: { - 'Content-Type': 'text/plain', - 'Authorization': `Basic ${auth}` - } - }; - - const req = http.request(options, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode >= 200 && res.statusCode < 300) { - resolve(data); - } else { - reject(new Error(`HTTP Error: ${res.statusCode} - ${data}`)); - } - }); - }); - - req.on('error', (error) => { - reject(error); - }); - - req.write(query); - req.end(); - }); -} - -// 检查ClickHouse服务器是否可用 -async function checkClickHouseConnection() { - console.log('检查ClickHouse连接...'); - - try { - const result = await sendClickHouseQuery('SELECT 1'); - console.log('ClickHouse连接成功'); - return true; - } catch (error) { - console.error('ClickHouse连接失败:', error.message); - return false; - } -} - -// 获取表的数据量 -async function getTableCount(tableName) { - try { - const result = await sendClickHouseQuery(`SELECT count() as count FROM ${tableName} FORMAT JSON`); - const data = JSON.parse(result); - return data.data[0].count; - } catch (error) { - console.error(`获取表 ${tableName} 数据量失败:`, error.message); - return 0; - } -} - -// 获取表的样本数据 -async function getTableSample(tableName, limit = 5) { - try { - const result = await sendClickHouseQuery(`SELECT * FROM ${tableName} LIMIT ${limit} FORMAT JSON`); - const data = JSON.parse(result); - return data.data; - } catch (error) { - console.error(`获取表 ${tableName} 样本数据失败:`, error.message); - return []; - } -} - -// 获取表的结构 -async function getTableStructure(tableName) { - try { - const result = await sendClickHouseQuery(`DESCRIBE TABLE ${tableName} FORMAT JSON`); - const data = JSON.parse(result); - return data.data; - } catch (error) { - console.error(`获取表 ${tableName} 结构失败:`, error.message); - return []; - } -} - -// 获取所有表 -async function getAllTables() { - try { - const result = await sendClickHouseQuery(` - SELECT name - FROM system.tables - WHERE database = '${process.env.CLICKHOUSE_DATABASE || 'promote'}' - FORMAT JSON - `); - const data = JSON.parse(result); - return data.data.map(row => row.name); - } catch (error) { - console.error('获取所有表失败:', error.message); - return []; - } -} - -// 主函数 -async function main() { - console.log('开始检查ClickHouse数据...'); - - try { - // 检查ClickHouse连接 - const connectionOk = await checkClickHouseConnection(); - if (!connectionOk) { - console.error('无法连接到ClickHouse服务器,请检查配置和服务器状态'); - return; - } - - // 获取所有表 - const tables = await getAllTables(); - console.log(`\n数据库中的表 (${tables.length}):`); - console.log(tables); - - // 检查每个表的数据 - for (const table of tables) { - console.log(`\n表: ${table}`); - - // 获取表结构 - const structure = await getTableStructure(table); - console.log('表结构:'); - console.table(structure.map(col => ({ - name: col.name, - type: col.type, - default_type: col.default_type, - default_expression: col.default_expression - }))); - - // 获取数据量 - const count = await getTableCount(table); - console.log(`数据量: ${count} 行`); - - // 获取样本数据 - if (count > 0) { - const samples = await getTableSample(table); - console.log('样本数据:'); - console.table(samples); - } - } - - console.log('\nClickHouse数据检查完成!'); - } catch (error) { - console.error('检查ClickHouse数据过程中发生错误:', error); - } finally { - // 关闭客户端连接 - await client.close(); - } -} - -// 执行主函数 -main(); \ No newline at end of file diff --git a/backend/scripts/check-postgres-projects.js b/backend/scripts/check-postgres-projects.js deleted file mode 100644 index bfd524a..0000000 --- a/backend/scripts/check-postgres-projects.js +++ /dev/null @@ -1,51 +0,0 @@ -require('dotenv').config(); -const { Pool } = require('pg'); - -// 创建PostgreSQL连接池 -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, -}); - -// 获取所有项目 -async function getAllProjects() { - try { - const client = await pool.connect(); - const result = await client.query('SELECT id, name, description, status FROM public.projects'); - client.release(); - - return result.rows; - } catch (error) { - console.error('获取项目失败:', error); - return []; - } -} - -// 主函数 -async function main() { - console.log('查询PostgreSQL数据库中的项目...'); - - try { - // 获取所有项目 - const projects = await getAllProjects(); - - if (projects.length === 0) { - console.log('没有找到任何项目,请先插入测试项目数据'); - } else { - console.log(`找到 ${projects.length} 个项目:`); - console.table(projects); - - // 提供一个示例项目ID,用于漏斗接口 - console.log('\n漏斗接口可以使用的项目ID示例:'); - console.log(`项目ID: ${projects[0].id}`); - console.log(`接口URL示例: http://localhost:4000/api/analytics/project/${projects[0].id}/conversion-funnel?timeRange=30days`); - } - } catch (error) { - console.error('查询项目过程中发生错误:', error); - } finally { - // 关闭连接池 - await pool.end(); - } -} - -// 执行主函数 -main(); \ No newline at end of file diff --git a/backend/scripts/check-clickhouse-schema.js b/backend/scripts/db-inspector/clickhouse-schema.js similarity index 69% rename from backend/scripts/check-clickhouse-schema.js rename to backend/scripts/db-inspector/clickhouse-schema.js index fb8805b..f5f319d 100644 --- a/backend/scripts/check-clickhouse-schema.js +++ b/backend/scripts/db-inspector/clickhouse-schema.js @@ -1,10 +1,14 @@ // 检查ClickHouse数据库结构的脚本 -const { ClickHouseClient } = require('@clickhouse/client'); +const { createClient } = require('@clickhouse/client'); const dotenv = require('dotenv'); const path = require('path'); +const fs = require('fs'); // 加载环境变量 -dotenv.config({ path: path.resolve(__dirname, '../.env') }); +dotenv.config({ path: path.resolve(__dirname, '../../.env') }); + +// 定义输出目录 +const DB_REPORTS_DIR = '/Users/liam/code/promote/backend/db-reports'; // 获取ClickHouse配置 const clickhouseHost = process.env.CLICKHOUSE_HOST || 'localhost'; @@ -19,9 +23,9 @@ console.log(` - 端口: ${clickhousePort}`); console.log(` - 用户: ${clickhouseUser}`); console.log(` - 数据库: ${clickhouseDatabase}`); -// 创建ClickHouse客户端 -const client = new ClickHouseClient({ - host: `http://${clickhouseHost}:${clickhousePort}`, +// 创建ClickHouse客户端 - 使用0.2.10版本的API +const client = createClient({ + url: `http://${clickhouseHost}:${clickhousePort}`, username: clickhouseUser, password: clickhousePassword, database: clickhouseDatabase @@ -135,6 +139,18 @@ async function getTableDataSample(tableName, limit = 5) { // 主函数 async function main() { + let outputBuffer = ''; + const originalConsoleLog = console.log; + + // 重定向console.log到buffer和控制台 + console.log = function() { + // 调用原始的console.log + originalConsoleLog.apply(console, arguments); + + // 写入到buffer + outputBuffer += Array.from(arguments).join(' ') + '\n'; + }; + try { // 获取所有表 const tables = await getAllTables(); @@ -144,20 +160,53 @@ async function main() { process.exit(1); } - // 获取每个表的结构和数据示例 + console.log('\n所有ClickHouse表:'); + console.log(tables.join(', ')); + + // 获取每个表的结构,但不获取数据示例 for (const tableName of tables) { await getTableSchema(tableName); - await getTableDataSample(tableName); + // 移除数据示例检查 + // await getTableDataSample(tableName); } console.log('\nClickHouse数据库结构检查完成'); + + // 保存输出到指定目录 + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + + // 确保目录存在 + if (!fs.existsSync(DB_REPORTS_DIR)) { + fs.mkdirSync(DB_REPORTS_DIR, { recursive: true }); + } + + const outputPath = path.join(DB_REPORTS_DIR, `clickhouse-schema-${timestamp}.log`); + fs.writeFileSync(outputPath, outputBuffer); + originalConsoleLog(`结果已保存到: ${outputPath}`); + } catch (error) { console.error('检查ClickHouse数据库结构时出错:', error); } finally { + // 恢复原始的console.log + console.log = originalConsoleLog; + // 关闭客户端连接 await client.close(); } } -// 运行主函数 -main(); \ No newline at end of file +// 导出函数 +module.exports = { + getAllTables, + getTableSchema, + getTableDataSample, + main +}; + +// 如果直接运行此脚本,则执行main函数 +if (require.main === module) { + main().catch(error => { + console.error('运行脚本时出错:', error); + process.exit(1); + }); +} \ No newline at end of file diff --git a/backend/scripts/check-db-schema.js b/backend/scripts/db-inspector/postgres-schema.js similarity index 59% rename from backend/scripts/check-db-schema.js rename to backend/scripts/db-inspector/postgres-schema.js index 4393660..ce64569 100644 --- a/backend/scripts/check-db-schema.js +++ b/backend/scripts/db-inspector/postgres-schema.js @@ -2,9 +2,10 @@ const { Client } = require('pg'); const dotenv = require('dotenv'); const path = require('path'); +const fs = require('fs'); // 加载环境变量 -dotenv.config({ path: path.resolve(__dirname, '../.env') }); +dotenv.config({ path: path.resolve(__dirname, '../../.env') }); // 获取数据库连接字符串 const databaseUrl = process.env.DATABASE_URL; @@ -14,15 +15,40 @@ if (!databaseUrl) { process.exit(1); } -console.log('使用PostgreSQL连接字符串连接数据库...'); +// 定义输出目录 +const DB_REPORTS_DIR = '/Users/liam/code/promote/backend/db-reports'; -// 创建PostgreSQL客户端 -const client = new Client({ - connectionString: databaseUrl, -}); +// 连接数据库 +async function connect() { + console.log('使用PostgreSQL连接字符串连接数据库...'); + + // 创建PostgreSQL客户端 + const client = new Client({ + connectionString: databaseUrl, + }); + + try { + await client.connect(); + console.log('成功连接到数据库'); + return client; + } catch (error) { + console.error('连接数据库失败:', error); + throw error; + } +} + +// 断开数据库连接 +async function disconnect(client) { + try { + await client.end(); + console.log('已断开数据库连接'); + } catch (error) { + console.error('断开数据库连接失败:', error); + } +} // 获取所有表 -async function getAllTables() { +async function getAllTables(client) { console.log('\n获取所有表...'); try { @@ -53,7 +79,7 @@ async function getAllTables() { } // 获取表结构 -async function getTableSchema(tableName) { +async function getTableSchema(client, tableName) { console.log(`\n获取表 ${tableName} 的结构...`); try { @@ -92,7 +118,7 @@ async function getTableSchema(tableName) { } // 获取表数据示例 -async function getTableDataSample(tableName, limit = 5) { +async function getTableDataSample(client, tableName, limit = 5) { console.log(`\n获取表 ${tableName} 的数据示例 (最多 ${limit} 行)...`); try { @@ -126,40 +152,89 @@ async function getTableDataSample(tableName, limit = 5) { // 主函数 async function main() { + let client = null; + let outputBuffer = ''; + const originalConsoleLog = console.log; + + // 重定向console.log到buffer和控制台 + console.log = function() { + // 调用原始的console.log + originalConsoleLog.apply(console, arguments); + + // 写入到buffer + outputBuffer += Array.from(arguments).join(' ') + '\n'; + }; + try { // 连接数据库 - await client.connect(); - console.log('成功连接到数据库'); + client = await connect(); // 获取所有表 - const tables = await getAllTables(); + const tables = await getAllTables(client); if (!tables) { console.error('无法获取表列表'); process.exit(1); } - // 获取我们需要的表的结构和数据示例 + console.log('\n所有PostgreSQL表:'); + console.log(tables.join(', ')); + + // 获取特定表的结构,但不获取数据示例 const requiredTables = ['projects', 'influencers', 'project_influencers', 'posts']; for (const tableName of requiredTables) { if (tables.includes(tableName)) { - await getTableSchema(tableName); - await getTableDataSample(tableName); + await getTableSchema(client, tableName); + // 移除数据示例检查 + // await getTableDataSample(client, tableName); } else { console.log(`\n表 ${tableName} 不存在`); } } console.log('\n数据库结构检查完成'); + + // 保存输出到指定目录 + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + + // 确保目录存在 + if (!fs.existsSync(DB_REPORTS_DIR)) { + fs.mkdirSync(DB_REPORTS_DIR, { recursive: true }); + } + + const outputPath = path.join(DB_REPORTS_DIR, `postgres-schema-${timestamp}.log`); + fs.writeFileSync(outputPath, outputBuffer); + originalConsoleLog(`结果已保存到: ${outputPath}`); + } catch (error) { console.error('检查数据库结构时出错:', error); process.exit(1); } finally { + // 恢复原始的console.log + console.log = originalConsoleLog; + // 关闭数据库连接 - await client.end(); + if (client) { + await disconnect(client); + } } } -// 运行主函数 -main(); \ No newline at end of file +// 导出函数 +module.exports = { + connect, + disconnect, + getAllTables, + getTableSchema, + getTableDataSample, + main +}; + +// 如果直接运行此脚本,则执行main函数 +if (require.main === module) { + main().catch(error => { + console.error('运行脚本时出错:', error); + process.exit(1); + }); +} \ No newline at end of file diff --git a/backend/scripts/db-inspector/run-all.js b/backend/scripts/db-inspector/run-all.js new file mode 100644 index 0000000..9de0618 --- /dev/null +++ b/backend/scripts/db-inspector/run-all.js @@ -0,0 +1,102 @@ +// 一键运行所有数据库检查脚本 +const { exec } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +// 定义脚本路径 +const postgresScriptPath = path.join(__dirname, 'postgres-schema.js'); +const clickhouseScriptPath = path.join(__dirname, 'clickhouse-schema.js'); + +// 定义输出目录 +const DB_REPORTS_DIR = '/Users/liam/code/promote/backend/db-reports'; + +// 确保目录存在 +if (!fs.existsSync(DB_REPORTS_DIR)) { + fs.mkdirSync(DB_REPORTS_DIR, { recursive: true }); + console.log(`创建输出目录: ${DB_REPORTS_DIR}`); +} + +// 定义日期时间格式化函数,用于生成日志文件名 +function getTimestampString() { + return new Date().toISOString().replace(/[:.]/g, '-'); +} + +// 运行PostgreSQL脚本 +async function runPostgresScript() { + return new Promise((resolve, reject) => { + console.log('\n======================================='); + console.log('正在运行PostgreSQL数据库结构检查脚本...'); + console.log('=======================================\n'); + + const process = exec(`node --no-inspect ${postgresScriptPath}`, (error, stdout, stderr) => { + if (error) { + console.error(`PostgreSQL脚本运行出错: ${error.message}`); + reject(error); + return; + } + + if (stderr) { + console.error(`PostgreSQL脚本错误: ${stderr}`); + } + + console.log(stdout); + resolve(); + }); + }); +} + +// 运行ClickHouse脚本 +async function runClickHouseScript() { + return new Promise((resolve, reject) => { + console.log('\n======================================='); + console.log('正在运行ClickHouse数据库结构检查脚本...'); + console.log('=======================================\n'); + + const process = exec(`node --no-inspect ${clickhouseScriptPath}`, (error, stdout, stderr) => { + if (error) { + console.error(`ClickHouse脚本运行出错: ${error.message}`); + reject(error); + return; + } + + if (stderr) { + console.error(`ClickHouse脚本错误: ${stderr}`); + } + + console.log(stdout); + resolve(); + }); + }); +} + +// 主函数 +async function main() { + try { + console.log('开始运行所有数据库结构检查脚本...'); + console.log(`输出目录: ${DB_REPORTS_DIR}`); + console.log(`时间戳: ${getTimestampString()}`); + + // 运行PostgreSQL脚本 + await runPostgresScript(); + + // 运行ClickHouse脚本 + await runClickHouseScript(); + + console.log('\n======================================='); + console.log('所有数据库结构检查脚本已完成!'); + console.log('报告已保存到以下目录:'); + console.log(DB_REPORTS_DIR); + console.log('======================================='); + } catch (error) { + console.error('运行脚本时出错:', error); + process.exit(1); + } +} + +// 执行主函数 +if (require.main === module) { + main().catch(error => { + console.error('运行脚本时出错:', error); + process.exit(1); + }); +} \ No newline at end of file diff --git a/backend/scripts/db-inspector/run-all.sh b/backend/scripts/db-inspector/run-all.sh new file mode 100755 index 0000000..e362caa --- /dev/null +++ b/backend/scripts/db-inspector/run-all.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# 一键运行所有数据库检查脚本 +echo "=============================================" +echo "开始运行所有数据库结构检查脚本..." +echo "=============================================" + +# 定义输出目录 +DB_REPORTS_DIR="/Users/liam/code/promote/backend/db-reports" + +# 确保目录存在 +mkdir -p "$DB_REPORTS_DIR" +echo "输出目录: $DB_REPORTS_DIR" + +# 获取当前时间戳 +TIMESTAMP=$(date +"%Y-%m-%dT%H-%M-%S") +echo "时间戳: $TIMESTAMP" +echo "" + +# 运行PostgreSQL脚本 +echo "=============================================" +echo "正在运行PostgreSQL数据库结构检查脚本..." +echo "=============================================" +NODE_OPTIONS="--no-deprecation --no-warnings" node --no-inspect backend/scripts/db-inspector/postgres-schema.js + +# 等待1秒 +sleep 1 + +# 运行ClickHouse脚本 +echo "" +echo "=============================================" +echo "正在运行ClickHouse数据库结构检查脚本..." +echo "=============================================" +NODE_OPTIONS="--no-deprecation --no-warnings" node --no-inspect backend/scripts/db-inspector/clickhouse-schema.js + +# 完成信息 +echo "" +echo "=============================================" +echo "所有数据库结构检查脚本已完成!" +echo "报告已保存到以下目录:" +echo "$DB_REPORTS_DIR" +echo "=============================================" + +# 列出生成的报告文件 +echo "" +echo "生成的报告文件:" +ls -la "$DB_REPORTS_DIR" \ No newline at end of file diff --git a/backend/scripts/generate-fake-data.ts b/backend/scripts/generate-fake-data.ts deleted file mode 100644 index 624fa25..0000000 --- a/backend/scripts/generate-fake-data.ts +++ /dev/null @@ -1,483 +0,0 @@ -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 diff --git a/backend/scripts/generate-funnel-test-data.js b/backend/scripts/generate-funnel-test-data.js deleted file mode 100644 index aae4ae0..0000000 --- a/backend/scripts/generate-funnel-test-data.js +++ /dev/null @@ -1,351 +0,0 @@ -// 生成KOL合作转换漏斗测试数据的脚本 -const { createClient } = require('@supabase/supabase-js'); -const dotenv = require('dotenv'); -const path = require('path'); - -// 加载环境变量 -dotenv.config({ path: path.resolve(__dirname, '../.env') }); - -// 创建Supabase客户端 -const supabaseUrl = process.env.SUPABASE_URL; -const supabaseKey = process.env.SUPABASE_KEY; - -if (!supabaseUrl || !supabaseKey) { - console.error('缺少Supabase配置。请确保.env文件中包含SUPABASE_URL和SUPABASE_KEY'); - process.exit(1); -} - -const supabase = createClient(supabaseUrl, supabaseKey); - -// 生成随机字符串 -const generateRandomString = (length = 8) => { - return Math.random().toString(36).substring(2, length + 2); -}; - -// 生成随机日期 -const generateRandomDate = (startDate, endDate) => { - const start = startDate.getTime(); - const end = endDate.getTime(); - const randomTime = start + Math.random() * (end - start); - return new Date(randomTime).toISOString(); -}; - -// 生成随机数字 -const generateRandomNumber = (min, max) => { - return Math.floor(Math.random() * (max - min + 1)) + min; -}; - -// 创建测试项目 -const createTestProject = async () => { - console.log('创建测试项目...'); - - const projectName = `漏斗测试项目-${generateRandomString()}`; - - const { data, error } = await supabase - .from('projects') - .insert({ - name: projectName, - description: '这是一个用于测试KOL合作转换漏斗API的项目', - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() - }) - .select() - .single(); - - if (error) { - console.error('创建测试项目失败:', error); - process.exit(1); - } - - console.log(`测试项目创建成功: ${data.name} (ID: ${data.id})`); - return data.id; -}; - -// 创建测试KOL -const createTestInfluencers = async (count) => { - console.log(`创建${count}个测试KOL...`); - - const platforms = ['instagram', 'youtube', 'tiktok', 'twitter', 'facebook']; - const influencers = []; - - // 创建不同阶段的KOL - // 1. 认知阶段 - 所有KOL (100%) - // 2. 兴趣阶段 - 75%的KOL有内容 - // 3. 考虑阶段 - 50%的KOL有高互动率 - // 4. 意向阶段 - 30%的KOL有多篇内容 - // 5. 评估阶段 - 20%的KOL有高浏览量 - // 6. 购买阶段 - 10%的KOL是长期合作(3个月以上) - - // 计算各阶段的KOL数量 - const awarenessCount = count; - const interestCount = Math.floor(count * 0.75); - const considerationCount = Math.floor(count * 0.5); - const intentCount = Math.floor(count * 0.3); - const evaluationCount = Math.floor(count * 0.2); - const purchaseCount = Math.floor(count * 0.1); - - // 创建所有KOL - for (let i = 0; i < count; i++) { - const platform = platforms[Math.floor(Math.random() * platforms.length)]; - - // 根据KOL所处阶段设置不同的创建日期 - let createdAt; - - if (i < purchaseCount) { - // 购买阶段 - 创建日期在3个月以前 - createdAt = generateRandomDate( - new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), // 1年前 - new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) // 3个月前 - ); - } else if (i < evaluationCount) { - // 评估阶段 - 创建日期在1-3个月之间 - createdAt = generateRandomDate( - new Date(Date.now() - 90 * 24 * 60 * 60 * 1000), // 3个月前 - new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // 1个月前 - ); - } else if (i < intentCount) { - // 意向阶段 - 创建日期在2周-1个月之间 - createdAt = generateRandomDate( - new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // 1个月前 - new Date(Date.now() - 14 * 24 * 60 * 60 * 1000) // 2周前 - ); - } else if (i < considerationCount) { - // 考虑阶段 - 创建日期在1-2周之间 - createdAt = generateRandomDate( - new Date(Date.now() - 14 * 24 * 60 * 60 * 1000), // 2周前 - new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // 1周前 - ); - } else if (i < interestCount) { - // 兴趣阶段 - 创建日期在3天-1周之间 - createdAt = generateRandomDate( - new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 1周前 - new Date(Date.now() - 3 * 24 * 60 * 60 * 1000) // 3天前 - ); - } else { - // 认知阶段 - 创建日期在3天内 - createdAt = generateRandomDate( - new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), // 3天前 - new Date() // 现在 - ); - } - - influencers.push({ - name: `测试KOL-${generateRandomString()}`, - platform, - profile_url: `https://${platform}.com/user${generateRandomString()}`, - followers_count: generateRandomNumber(1000, 1000000), - created_at: createdAt, - updated_at: new Date().toISOString() - }); - } - - const { data, error } = await supabase - .from('influencers') - .insert(influencers) - .select(); - - if (error) { - console.error('创建测试KOL失败:', error); - process.exit(1); - } - - console.log(`${data.length}个测试KOL创建成功`); - return data; -}; - -// 将KOL添加到项目 -const addInfluencersToProject = async (projectId, influencers) => { - console.log(`将KOL添加到项目 ${projectId}...`); - - const projectInfluencers = influencers.map(influencer => ({ - project_id: projectId, - influencer_id: influencer.id, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() - })); - - const { data, error } = await supabase - .from('project_influencers') - .insert(projectInfluencers) - .select(); - - if (error) { - console.error('将KOL添加到项目失败:', error); - process.exit(1); - } - - console.log(`${data.length}个KOL成功添加到项目`); - return data; -}; - -// 创建测试内容 -const createTestPosts = async (projectId, influencers) => { - console.log(`为项目 ${projectId} 创建测试内容...`); - - const posts = []; - - // 为不同阶段的KOL创建不同数量的内容 - const awarenessCount = influencers.length; - const interestCount = Math.floor(influencers.length * 0.75); - const considerationCount = Math.floor(influencers.length * 0.5); - const intentCount = Math.floor(influencers.length * 0.3); - const evaluationCount = Math.floor(influencers.length * 0.2); - - for (let i = 0; i < influencers.length; i++) { - const influencer = influencers[i]; - - // 根据KOL所处阶段创建不同数量的内容 - let postCount; - - if (i < evaluationCount) { - // 评估阶段 - 3-5篇内容 - postCount = generateRandomNumber(3, 5); - } else if (i < intentCount) { - // 意向阶段 - 2-3篇内容 - postCount = generateRandomNumber(2, 3); - } else if (i < considerationCount) { - // 考虑阶段 - 1-2篇内容 - postCount = generateRandomNumber(1, 2); - } else if (i < interestCount) { - // 兴趣阶段 - 1篇内容 - postCount = 1; - } else { - // 认知阶段 - 无内容 - postCount = 0; - } - - for (let j = 0; j < postCount; j++) { - const publishedAt = generateRandomDate( - new Date(Date.now() - 90 * 24 * 60 * 60 * 1000), // 3个月前 - new Date() // 现在 - ); - - posts.push({ - project_id: projectId, - influencer_id: influencer.id, - platform: influencer.platform, - title: `测试内容-${generateRandomString()}`, - description: `这是KOL ${influencer.name} 的测试内容`, - post_url: `https://${influencer.platform}.com/post/${generateRandomString()}`, - published_at: publishedAt, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() - }); - } - } - - if (posts.length === 0) { - console.log('没有创建任何内容'); - return []; - } - - const { data, error } = await supabase - .from('posts') - .insert(posts) - .select(); - - if (error) { - console.error('创建测试内容失败:', error); - process.exit(1); - } - - console.log(`${data.length}篇测试内容创建成功`); - return data; -}; - -// 测试KOL合作转换漏斗API -const testConversionFunnelAPI = async (projectId) => { - console.log(`测试KOL合作转换漏斗API,项目ID: ${projectId}...`); - - try { - const url = `http://localhost:4000/api/analytics/project/${projectId}/conversion-funnel`; - console.log(`请求URL: ${url}`); - - // 使用http模块发送请求 - const http = require('http'); - - return new Promise((resolve, reject) => { - const req = http.get(url, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode !== 200) { - console.error(`API请求失败: ${res.statusCode}`); - reject(new Error(`API请求失败: ${res.statusCode}`)); - return; - } - - try { - const jsonData = JSON.parse(data); - console.log('API响应:'); - console.log(JSON.stringify(jsonData, null, 2)); - resolve(jsonData); - } catch (error) { - console.error('解析API响应失败:', error); - reject(error); - } - }); - }); - - req.on('error', (error) => { - console.error('请求出错:', error); - reject(error); - }); - - req.end(); - }); - } catch (error) { - console.error('测试API失败:', error); - console.log('请确保后端服务器正在运行,并且可以访问API端点'); - console.log('运行命令: cd /Users/liam/code/promote/backend && npm run dev'); - } -}; - -// 主函数 -const main = async () => { - try { - // 创建测试项目 - const projectId = await createTestProject(); - - // 创建测试KOL - 创建100个KOL以便有足够的数据来测试漏斗的各个阶段 - const influencers = await createTestInfluencers(100); - - // 将KOL添加到项目 - await addInfluencersToProject(projectId, influencers); - - // 创建测试内容 - await createTestPosts(projectId, influencers); - - console.log('\n测试数据生成完成!'); - console.log(`项目ID: ${projectId}`); - console.log('KOL数量: 100'); - console.log('内容数量: 根据KOL所处阶段不同'); - console.log('\n漏斗阶段分布:'); - console.log('- 认知阶段 (Awareness): 100个KOL (100%)'); - console.log('- 兴趣阶段 (Interest): 75个KOL (75%)'); - console.log('- 考虑阶段 (Consideration): 50个KOL (50%)'); - console.log('- 意向阶段 (Intent): 30个KOL (30%)'); - console.log('- 评估阶段 (Evaluation): 20个KOL (20%)'); - console.log('- 购买阶段 (Purchase): 10个KOL (10%)'); - - console.log('\n现在您可以使用以下命令测试KOL合作转换漏斗API:'); - console.log(`curl http://localhost:4000/api/analytics/project/${projectId}/conversion-funnel`); - console.log('\n或者在浏览器中访问Swagger UI:'); - console.log('http://localhost:4000/swagger'); - - // 尝试测试API - console.log('\n尝试测试API...'); - await testConversionFunnelAPI(projectId); - - } catch (error) { - console.error('测试数据生成过程中出错:', error); - process.exit(1); - } -}; - -// 运行主函数 -main(); \ No newline at end of file diff --git a/backend/scripts/insert-clickhouse-test-data.js b/backend/scripts/insert-clickhouse-test-data.js deleted file mode 100644 index 44ea07c..0000000 --- a/backend/scripts/insert-clickhouse-test-data.js +++ /dev/null @@ -1,309 +0,0 @@ -require('dotenv').config(); -const { createClient } = require('@clickhouse/client'); -const { v4: uuidv4 } = require('uuid'); -const http = require('http'); - -// 创建ClickHouse客户端 -const client = createClient({ - host: `http://${process.env.CLICKHOUSE_HOST || 'localhost'}:${process.env.CLICKHOUSE_PORT || 8123}`, - username: process.env.CLICKHOUSE_USER || 'default', - password: process.env.CLICKHOUSE_PASSWORD || '', - database: process.env.CLICKHOUSE_DATABASE || 'promote', -}); - -// 生成随机日期,在指定天数范围内,返回格式化的日期字符串 -function randomDate(daysBack = 30) { - const date = new Date(); - date.setDate(date.getDate() - Math.floor(Math.random() * daysBack)); - return date.toISOString().slice(0, 19).replace('T', ' '); // 格式: YYYY-MM-DD HH:MM:SS -} - -// 生成随机数字,在指定范围内 -function randomNumber(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -// 生成随机IP地址 -function randomIP() { - return `${randomNumber(1, 255)}.${randomNumber(0, 255)}.${randomNumber(0, 255)}.${randomNumber(0, 255)}`; -} - -// 生成随机用户代理字符串 -function randomUserAgent() { - const browsers = [ - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15', - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0', - 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1', - 'Mozilla/5.0 (iPad; CPU OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1' - ]; - return browsers[randomNumber(0, browsers.length - 1)]; -} - -// 使用HTTP直接发送请求到ClickHouse -function sendClickHouseQuery(query) { - return new Promise((resolve, reject) => { - // 添加认证信息 - const username = process.env.CLICKHOUSE_USER || 'default'; - const password = process.env.CLICKHOUSE_PASSWORD || ''; - const auth = Buffer.from(`${username}:${password}`).toString('base64'); - - const options = { - hostname: process.env.CLICKHOUSE_HOST || 'localhost', - port: process.env.CLICKHOUSE_PORT || 8123, - path: `/?database=${process.env.CLICKHOUSE_DATABASE || 'promote'}`, - method: 'POST', - headers: { - 'Content-Type': 'text/plain', - 'Authorization': `Basic ${auth}` - } - }; - - const req = http.request(options, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - if (res.statusCode >= 200 && res.statusCode < 300) { - resolve(data); - } else { - reject(new Error(`HTTP Error: ${res.statusCode} - ${data}`)); - } - }); - }); - - req.on('error', (error) => { - reject(error); - }); - - req.write(query); - req.end(); - }); -} - -// 检查ClickHouse服务器是否可用 -async function checkClickHouseConnection() { - console.log('检查ClickHouse连接...'); - - try { - const result = await sendClickHouseQuery('SELECT 1'); - console.log('ClickHouse连接成功'); - return true; - } catch (error) { - console.error('ClickHouse连接失败:', error.message); - return false; - } -} - -// 检查ClickHouse表是否存在 -async function checkAndCreateTables() { - console.log('检查ClickHouse表是否存在...'); - - try { - // 创建view_events表 - await sendClickHouseQuery(` - CREATE TABLE IF NOT EXISTS view_events ( - user_id String, - content_id String, - timestamp DateTime DEFAULT now(), - ip String, - user_agent String - ) ENGINE = MergeTree() - PARTITION BY toYYYYMM(timestamp) - ORDER BY (user_id, content_id, timestamp) - `); - - // 创建like_events表 - await sendClickHouseQuery(` - CREATE TABLE IF NOT EXISTS like_events ( - user_id String, - content_id String, - timestamp DateTime DEFAULT now(), - action UInt8 - ) ENGINE = MergeTree() - PARTITION BY toYYYYMM(timestamp) - ORDER BY (user_id, content_id, timestamp) - `); - - // 创建follower_events表 - await sendClickHouseQuery(` - CREATE TABLE IF NOT EXISTS follower_events ( - follower_id String, - followed_id String, - timestamp DateTime DEFAULT now(), - action UInt8 - ) ENGINE = MergeTree() - PARTITION BY toYYYYMM(timestamp) - ORDER BY (follower_id, followed_id, timestamp) - `); - - console.log('表检查完成'); - return true; - } catch (error) { - console.error('检查或创建表失败:', error); - return false; - } -} - -// 插入测试浏览事件数据 -async function insertViewEvents(count = 100) { - console.log(`开始插入${count}个浏览事件...`); - - try { - // 每批次插入的数量 - const batchSize = 10; - const batches = Math.ceil(count / batchSize); - - for (let batch = 0; batch < batches; batch++) { - const startIdx = batch * batchSize; - const endIdx = Math.min(startIdx + batchSize, count); - const batchCount = endIdx - startIdx; - - let query = 'INSERT INTO view_events (user_id, content_id, timestamp, ip, user_agent) VALUES '; - - for (let i = 0; i < batchCount; i++) { - const userId = `user_${randomNumber(1, 100)}`; - const contentId = `content_${randomNumber(1, 50)}`; - const timestamp = randomDate(30); - const ip = randomIP(); - const userAgent = randomUserAgent().replace(/'/g, "\\'"); // 转义单引号 - - query += `('${userId}', '${contentId}', '${timestamp}', '${ip}', '${userAgent}')`; - - if (i < batchCount - 1) { - query += ', '; - } - } - - await sendClickHouseQuery(query); - console.log(`已插入 ${Math.min((batch + 1) * batchSize, count)} 个浏览事件...`); - } - - console.log(`成功插入${count}个浏览事件`); - return true; - } catch (error) { - console.error('插入浏览事件失败:', error); - return false; - } -} - -// 插入测试点赞事件数据 -async function insertLikeEvents(count = 50) { - console.log(`开始插入${count}个点赞事件...`); - - try { - // 每批次插入的数量 - const batchSize = 10; - const batches = Math.ceil(count / batchSize); - - for (let batch = 0; batch < batches; batch++) { - const startIdx = batch * batchSize; - const endIdx = Math.min(startIdx + batchSize, count); - const batchCount = endIdx - startIdx; - - let query = 'INSERT INTO like_events (user_id, content_id, timestamp, action) VALUES '; - - for (let i = 0; i < batchCount; i++) { - const userId = `user_${randomNumber(1, 100)}`; - const contentId = `content_${randomNumber(1, 50)}`; - const timestamp = randomDate(30); - const action = randomNumber(1, 10) <= 8 ? 1 : 2; // 80%是点赞,20%是取消点赞 - - query += `('${userId}', '${contentId}', '${timestamp}', ${action})`; - - if (i < batchCount - 1) { - query += ', '; - } - } - - await sendClickHouseQuery(query); - console.log(`已插入 ${Math.min((batch + 1) * batchSize, count)} 个点赞事件...`); - } - - console.log(`成功插入${count}个点赞事件`); - return true; - } catch (error) { - console.error('插入点赞事件失败:', error); - return false; - } -} - -// 插入测试关注事件数据 -async function insertFollowerEvents(count = 30) { - console.log(`开始插入${count}个关注事件...`); - - try { - // 每批次插入的数量 - const batchSize = 10; - const batches = Math.ceil(count / batchSize); - - for (let batch = 0; batch < batches; batch++) { - const startIdx = batch * batchSize; - const endIdx = Math.min(startIdx + batchSize, count); - const batchCount = endIdx - startIdx; - - let query = 'INSERT INTO follower_events (follower_id, followed_id, timestamp, action) VALUES '; - - for (let i = 0; i < batchCount; i++) { - const followerId = `user_${randomNumber(1, 100)}`; - const followedId = `influencer_${randomNumber(1, 20)}`; - const timestamp = randomDate(30); - const action = randomNumber(1, 10) <= 8 ? 1 : 2; // 80%是关注,20%是取消关注 - - query += `('${followerId}', '${followedId}', '${timestamp}', ${action})`; - - if (i < batchCount - 1) { - query += ', '; - } - } - - await sendClickHouseQuery(query); - console.log(`已插入 ${Math.min((batch + 1) * batchSize, count)} 个关注事件...`); - } - - console.log(`成功插入${count}个关注事件`); - return true; - } catch (error) { - console.error('插入关注事件失败:', error); - return false; - } -} - -// 主函数 -async function main() { - console.log('开始插入ClickHouse测试数据...'); - - try { - // 检查ClickHouse连接 - const connectionOk = await checkClickHouseConnection(); - if (!connectionOk) { - console.error('无法连接到ClickHouse服务器,请检查配置和服务器状态'); - return; - } - - // 检查并创建表 - await checkAndCreateTables(); - - // 插入测试浏览事件 - await insertViewEvents(100); - - // 插入测试点赞事件 - await insertLikeEvents(50); - - // 插入测试关注事件 - await insertFollowerEvents(30); - - console.log('所有ClickHouse测试数据插入完成!'); - } catch (error) { - console.error('插入ClickHouse测试数据过程中发生错误:', error); - } finally { - // 关闭客户端连接 - await client.close(); - } -} - -// 执行主函数 -main(); \ No newline at end of file diff --git a/backend/scripts/insert-test-data.js b/backend/scripts/insert-test-data.js deleted file mode 100644 index 11a5270..0000000 --- a/backend/scripts/insert-test-data.js +++ /dev/null @@ -1,311 +0,0 @@ -require('dotenv').config(); -const { Pool } = require('pg'); -const { v4: uuidv4 } = require('uuid'); - -// 创建PostgreSQL连接池 -const pool = new Pool({ - connectionString: process.env.DATABASE_URL, -}); - -// 生成随机日期,在指定天数范围内 -function randomDate(daysBack = 30) { - const date = new Date(); - date.setDate(date.getDate() - Math.floor(Math.random() * daysBack)); - return date.toISOString(); -} - -// 生成随机数字,在指定范围内 -function randomNumber(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -// 插入测试项目数据 -async function insertTestProjects(count = 5) { - console.log(`开始插入${count}个测试项目...`); - - const projectNames = [ - '夏季新品推广', '618电商活动', '品牌周年庆', '新产品发布会', - '冬季促销活动', '跨年营销', '校园推广', '明星代言合作', - '社交媒体挑战赛', '用户生成内容活动' - ]; - - const projectDescriptions = [ - '推广夏季新品系列,提高品牌知名度', - '618电商大促活动,提升销售转化', - '品牌成立周年庆典,增强品牌忠诚度', - '新产品线上发布会,扩大产品影响力', - '冬季产品促销活动,刺激季节性消费', - '跨年营销活动,提升品牌曝光', - '针对大学生群体的校园推广活动', - '与明星合作的品牌代言项目', - '社交媒体平台的用户参与挑战活动', - '鼓励用户创建与品牌相关内容的活动' - ]; - - const statuses = ['active', 'completed', 'archived']; - - try { - const client = await pool.connect(); - - for (let i = 0; i < count; i++) { - const nameIndex = i % projectNames.length; - const descIndex = i % projectDescriptions.length; - const statusIndex = i % statuses.length; - - const projectId = uuidv4(); - const startDate = randomDate(60); - const endDate = new Date(new Date(startDate).getTime() + (30 * 24 * 60 * 60 * 1000)).toISOString(); - - await client.query( - `INSERT INTO public.projects - (id, name, description, status, start_date, end_date, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, - [ - projectId, - projectNames[nameIndex], - projectDescriptions[descIndex], - statuses[statusIndex], - startDate, - endDate, - new Date().toISOString(), - new Date().toISOString() - ] - ); - - console.log(`已插入项目: ${projectNames[nameIndex]}`); - } - - client.release(); - console.log(`成功插入${count}个测试项目`); - return true; - } catch (error) { - console.error('插入测试项目失败:', error); - return false; - } -} - -// 插入测试网红数据 -async function insertTestInfluencers(count = 10) { - console.log(`开始插入${count}个测试网红...`); - - const influencerNames = [ - '张小明', '李华', '王芳', '刘星', '陈晓', - 'Emma Wong', 'Jack Chen', 'Sophia Liu', 'Noah Zhang', 'Olivia Wang', - '김민준', '이지은', '박서준', '최수지', '정우성' - ]; - - const platforms = ['youtube', 'instagram', 'tiktok', 'twitter', 'facebook']; - - try { - const client = await pool.connect(); - - for (let i = 0; i < count; i++) { - const nameIndex = i % influencerNames.length; - const platformIndex = i % platforms.length; - const platform = platforms[platformIndex]; - - const influencerId = uuidv4(); - const externalId = `${platform}_${Math.random().toString(36).substring(2, 10)}`; - const followersCount = randomNumber(1000, 1000000); - const videoCount = randomNumber(10, 500); - - await client.query( - `INSERT INTO public.influencers - (influencer_id, name, platform, profile_url, external_id, followers_count, video_count, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, - [ - influencerId, - influencerNames[nameIndex], - platform, - `https://${platform}.com/${externalId}`, - externalId, - followersCount, - videoCount, - new Date().toISOString(), - new Date().toISOString() - ] - ); - - console.log(`已插入网红: ${influencerNames[nameIndex]} (${platform})`); - } - - client.release(); - console.log(`成功插入${count}个测试网红`); - return true; - } catch (error) { - console.error('插入测试网红失败:', error); - return false; - } -} - -// 关联项目和网红 -async function associateProjectsAndInfluencers() { - console.log('开始关联项目和网红...'); - - try { - const client = await pool.connect(); - - // 获取所有项目 - const projectsResult = await client.query('SELECT id FROM public.projects'); - const projects = projectsResult.rows; - - // 获取所有网红 - const influencersResult = await client.query('SELECT influencer_id FROM public.influencers'); - const influencers = influencersResult.rows; - - if (projects.length === 0 || influencers.length === 0) { - console.log('没有找到项目或网红数据,无法创建关联'); - client.release(); - return false; - } - - const statuses = ['active', 'inactive', 'completed']; - let associationsCount = 0; - - // 为每个项目随机关联1-5个网红 - for (const project of projects) { - const numInfluencers = randomNumber(1, 5); - const shuffledInfluencers = [...influencers].sort(() => 0.5 - Math.random()); - const selectedInfluencers = shuffledInfluencers.slice(0, numInfluencers); - - for (const influencer of selectedInfluencers) { - const statusIndex = randomNumber(0, statuses.length - 1); - - try { - await client.query( - `INSERT INTO public.project_influencers - (id, project_id, influencer_id, status, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6)`, - [ - uuidv4(), - project.id, - influencer.influencer_id, - statuses[statusIndex], - new Date().toISOString(), - new Date().toISOString() - ] - ); - - associationsCount++; - } catch (error) { - // 可能是唯一约束冲突,忽略错误继续 - console.log(`关联已存在或发生错误: ${error.message}`); - } - } - } - - client.release(); - console.log(`成功创建${associationsCount}个项目-网红关联`); - return true; - } catch (error) { - console.error('关联项目和网红失败:', error); - return false; - } -} - -// 插入测试帖子数据 -async function insertTestPosts(postsPerInfluencer = 3) { - console.log(`开始为每个网红插入${postsPerInfluencer}个测试帖子...`); - - const postTitles = [ - '新品开箱视频', '使用体验分享', '产品评测', '购物分享', - '日常VLOG', '挑战视频', '教程分享', '合作推广', - '直播回放', '问答视频' - ]; - - const postDescriptions = [ - '今天为大家带来新品开箱,这款产品真的太赞了!', - '使用一周后的真实体验分享,优缺点都告诉你', - '专业评测:性能、外观、性价比全面分析', - '本月最值得购买的好物推荐,不容错过', - '跟我一起度过充实的一天,生活记录分享', - '参与最新网络挑战,太有趣了', - '详细教程:如何正确使用这款产品', - '与品牌合作的特别内容,限时优惠', - '错过直播的朋友可以看回放啦', - '回答粉丝提问,解答产品使用疑惑' - ]; - - try { - const client = await pool.connect(); - - // 获取所有网红 - const influencersResult = await client.query('SELECT influencer_id, platform FROM public.influencers'); - const influencers = influencersResult.rows; - - if (influencers.length === 0) { - console.log('没有找到网红数据,无法创建帖子'); - client.release(); - return false; - } - - let postsCount = 0; - - // 为每个网红创建帖子 - for (const influencer of influencers) { - for (let i = 0; i < postsPerInfluencer; i++) { - const titleIndex = randomNumber(0, postTitles.length - 1); - const descIndex = randomNumber(0, postDescriptions.length - 1); - const publishedDate = randomDate(30); - - const postId = uuidv4(); - const postUrl = `https://${influencer.platform}.com/post/${Math.random().toString(36).substring(2, 10)}`; - - await client.query( - `INSERT INTO public.posts - (post_id, influencer_id, platform, post_url, title, description, published_at, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, - [ - postId, - influencer.influencer_id, - influencer.platform, - postUrl, - postTitles[titleIndex], - postDescriptions[descIndex], - publishedDate, - new Date().toISOString(), - new Date().toISOString() - ] - ); - - postsCount++; - } - } - - client.release(); - console.log(`成功插入${postsCount}个测试帖子`); - return true; - } catch (error) { - console.error('插入测试帖子失败:', error); - return false; - } -} - -// 主函数 -async function main() { - console.log('开始插入测试数据...'); - - try { - // 插入测试项目 - await insertTestProjects(5); - - // 插入测试网红 - await insertTestInfluencers(15); - - // 关联项目和网红 - await associateProjectsAndInfluencers(); - - // 插入测试帖子 - await insertTestPosts(5); - - console.log('所有测试数据插入完成!'); - } catch (error) { - console.error('插入测试数据过程中发生错误:', error); - } finally { - // 关闭连接池 - await pool.end(); - } -} - -// 执行主函数 -main(); \ No newline at end of file diff --git a/backend/scripts/test-conversion-funnel.js b/backend/scripts/test-conversion-funnel.js deleted file mode 100644 index 1a234af..0000000 --- a/backend/scripts/test-conversion-funnel.js +++ /dev/null @@ -1,111 +0,0 @@ -// 测试KOL合作转换漏斗API的脚本 -const { exec } = require('child_process'); -const http = require('http'); -const https = require('https'); -const { URL } = require('url'); - -// 测试项目ID - 可以手动设置一个已知的项目ID -const TEST_PROJECT_ID = '1'; // 替换为实际的项目ID - -// 简单的HTTP请求函数 -function httpRequest(url, options = {}) { - return new Promise((resolve, reject) => { - const parsedUrl = new URL(url); - const protocol = parsedUrl.protocol === 'https:' ? https : http; - - const req = protocol.request(url, options, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - try { - const jsonData = JSON.parse(data); - resolve({ statusCode: res.statusCode, data: jsonData }); - } catch (error) { - resolve({ statusCode: res.statusCode, data }); - } - }); - }); - - req.on('error', (error) => { - reject(error); - }); - - if (options.body) { - req.write(JSON.stringify(options.body)); - } - - req.end(); - }); -} - -// 测试KOL合作转换漏斗API -async function testConversionFunnelAPI(projectId) { - console.log(`测试KOL合作转换漏斗API,项目ID: ${projectId}...`); - - try { - const url = `http://localhost:4000/api/analytics/project/${projectId}/conversion-funnel`; - console.log(`请求URL: ${url}`); - - const response = await httpRequest(url); - - if (response.statusCode !== 200) { - throw new Error(`API请求失败: ${response.statusCode}`); - } - - console.log('API响应:'); - console.log(JSON.stringify(response.data, null, 2)); - - return response.data; - } catch (error) { - console.error('测试API失败:', error.message); - process.exit(1); - } -} - -// 检查后端服务器是否正在运行 -function checkServerRunning() { - return new Promise((resolve, reject) => { - const req = http.request('http://localhost:4000/health', { method: 'GET' }, (res) => { - if (res.statusCode === 200) { - resolve(true); - } else { - resolve(false); - } - }); - - req.on('error', () => { - resolve(false); - }); - - req.end(); - }); -} - -// 主函数 -async function main() { - try { - // 检查服务器是否运行 - const isServerRunning = await checkServerRunning(); - - if (!isServerRunning) { - console.log('后端服务器未运行,请先启动服务器'); - console.log('运行命令: cd /Users/liam/code/promote/backend && npm run dev'); - process.exit(1); - } - - // 测试API - await testConversionFunnelAPI(TEST_PROJECT_ID); - - console.log('测试完成!'); - } catch (error) { - console.error('测试过程中出错:', error.message); - process.exit(1); - } -} - -// 运行主函数 -main(); \ No newline at end of file diff --git a/backend/scripts/test-funnel-with-mock-data.js b/backend/scripts/test-funnel-with-mock-data.js deleted file mode 100644 index 959bda4..0000000 --- a/backend/scripts/test-funnel-with-mock-data.js +++ /dev/null @@ -1,110 +0,0 @@ -// 测试KOL合作转换漏斗API的脚本,使用模拟的项目ID -const http = require('http'); - -// 模拟的项目ID列表 -const MOCK_PROJECT_IDS = [ - '1', // 简单数字ID - 'test-project-id', // 测试项目ID - '550e8400-e29b-41d4-a716-446655440000', // UUID格式 -]; - -// 简单的HTTP请求函数 -function httpRequest(url, options = {}) { - return new Promise((resolve, reject) => { - const req = http.request(url, options, (res) => { - let data = ''; - - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - try { - const jsonData = JSON.parse(data); - resolve({ statusCode: res.statusCode, data: jsonData }); - } catch (error) { - resolve({ statusCode: res.statusCode, data }); - } - }); - }); - - req.on('error', (error) => { - reject(error); - }); - - if (options.body) { - req.write(JSON.stringify(options.body)); - } - - req.end(); - }); -} - -// 测试KOL合作转换漏斗API -async function testConversionFunnelAPI(projectId) { - console.log(`测试KOL合作转换漏斗API,项目ID: ${projectId}...`); - - try { - const url = `http://localhost:4000/api/analytics/project/${projectId}/conversion-funnel`; - console.log(`请求URL: ${url}`); - - const response = await httpRequest(url); - - console.log(`状态码: ${response.statusCode}`); - console.log('API响应:'); - console.log(JSON.stringify(response.data, null, 2)); - - return response.data; - } catch (error) { - console.error('测试API失败:', error.message); - return null; - } -} - -// 检查后端服务器是否正在运行 -async function checkServerRunning() { - try { - const response = await httpRequest('http://localhost:4000/health', { method: 'GET' }); - return response.statusCode === 200; - } catch (error) { - return false; - } -} - -// 主函数 -async function main() { - try { - // 检查服务器是否运行 - const isServerRunning = await checkServerRunning(); - - if (!isServerRunning) { - console.log('后端服务器未运行,请先启动服务器'); - console.log('运行命令: cd /Users/liam/code/promote/backend && npm run dev'); - process.exit(1); - } - - console.log('后端服务器正在运行,开始测试漏斗接口...\n'); - - // 测试所有模拟的项目ID - for (const projectId of MOCK_PROJECT_IDS) { - console.log(`\n===== 测试项目ID: ${projectId} =====\n`); - await testConversionFunnelAPI(projectId); - } - - console.log('\n测试完成!'); - console.log('\n提示: 如果所有测试都返回404错误,说明服务器无法找到项目。'); - console.log('这可能是因为:'); - console.log('1. 数据库中没有这些项目ID'); - console.log('2. 无法连接到数据库'); - console.log('3. 漏斗接口没有实现模拟数据功能'); - console.log('\n建议:'); - console.log('1. 在前端代码中使用硬编码的项目ID: "1"'); - console.log('2. 修改漏斗接口,在找不到项目时返回模拟数据'); - } catch (error) { - console.error('测试过程中出错:', error.message); - process.exit(1); - } -} - -// 运行主函数 -main(); \ No newline at end of file