get db schema
This commit is contained in:
@@ -3,7 +3,7 @@ PORT=4000
|
|||||||
SUPABASE_URL="https://xtqhluzornazlmkonucr.supabase.co"
|
SUPABASE_URL="https://xtqhluzornazlmkonucr.supabase.co"
|
||||||
SUPABASE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0cWhsdXpvcm5hemxta29udWNyIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTI0NDcxMywiZXhwIjoyMDU2ODIwNzEzfQ.E9lHlmdoqCZ9zYg0ammuW6ua-__tEEATCrwYv3-Th3I"
|
SUPABASE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0cWhsdXpvcm5hemxta29udWNyIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTI0NDcxMywiZXhwIjoyMDU2ODIwNzEzfQ.E9lHlmdoqCZ9zYg0ammuW6ua-__tEEATCrwYv3-Th3I"
|
||||||
SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0cWhsdXpvcm5hemxta29udWNyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEyNDQ3MTMsImV4cCI6MjA1NjgyMDcxM30.PUXbyHOgkvFE6T5fvCFTV7LJq-MbMkRtNdw2VoKJOAg"
|
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_HOST="localhost"
|
||||||
REDIS_PORT="6379"
|
REDIS_PORT="6379"
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
// 检查ClickHouse数据库结构的脚本
|
// 检查ClickHouse数据库结构的脚本
|
||||||
const { ClickHouseClient } = require('@clickhouse/client');
|
const { createClient } = require('@clickhouse/client');
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
const path = require('path');
|
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配置
|
// 获取ClickHouse配置
|
||||||
const clickhouseHost = process.env.CLICKHOUSE_HOST || 'localhost';
|
const clickhouseHost = process.env.CLICKHOUSE_HOST || 'localhost';
|
||||||
@@ -19,9 +23,9 @@ console.log(` - 端口: ${clickhousePort}`);
|
|||||||
console.log(` - 用户: ${clickhouseUser}`);
|
console.log(` - 用户: ${clickhouseUser}`);
|
||||||
console.log(` - 数据库: ${clickhouseDatabase}`);
|
console.log(` - 数据库: ${clickhouseDatabase}`);
|
||||||
|
|
||||||
// 创建ClickHouse客户端
|
// 创建ClickHouse客户端 - 使用0.2.10版本的API
|
||||||
const client = new ClickHouseClient({
|
const client = createClient({
|
||||||
host: `http://${clickhouseHost}:${clickhousePort}`,
|
url: `http://${clickhouseHost}:${clickhousePort}`,
|
||||||
username: clickhouseUser,
|
username: clickhouseUser,
|
||||||
password: clickhousePassword,
|
password: clickhousePassword,
|
||||||
database: clickhouseDatabase
|
database: clickhouseDatabase
|
||||||
@@ -135,6 +139,18 @@ async function getTableDataSample(tableName, limit = 5) {
|
|||||||
|
|
||||||
// 主函数
|
// 主函数
|
||||||
async function main() {
|
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 {
|
try {
|
||||||
// 获取所有表
|
// 获取所有表
|
||||||
const tables = await getAllTables();
|
const tables = await getAllTables();
|
||||||
@@ -144,20 +160,53 @@ async function main() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取每个表的结构和数据示例
|
console.log('\n所有ClickHouse表:');
|
||||||
|
console.log(tables.join(', '));
|
||||||
|
|
||||||
|
// 获取每个表的结构,但不获取数据示例
|
||||||
for (const tableName of tables) {
|
for (const tableName of tables) {
|
||||||
await getTableSchema(tableName);
|
await getTableSchema(tableName);
|
||||||
await getTableDataSample(tableName);
|
// 移除数据示例检查
|
||||||
|
// await getTableDataSample(tableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\nClickHouse数据库结构检查完成');
|
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) {
|
} catch (error) {
|
||||||
console.error('检查ClickHouse数据库结构时出错:', error);
|
console.error('检查ClickHouse数据库结构时出错:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
// 恢复原始的console.log
|
||||||
|
console.log = originalConsoleLog;
|
||||||
|
|
||||||
// 关闭客户端连接
|
// 关闭客户端连接
|
||||||
await client.close();
|
await client.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 运行主函数
|
// 导出函数
|
||||||
main();
|
module.exports = {
|
||||||
|
getAllTables,
|
||||||
|
getTableSchema,
|
||||||
|
getTableDataSample,
|
||||||
|
main
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果直接运行此脚本,则执行main函数
|
||||||
|
if (require.main === module) {
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('运行脚本时出错:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,9 +2,10 @@
|
|||||||
const { Client } = require('pg');
|
const { Client } = require('pg');
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
const path = require('path');
|
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;
|
const databaseUrl = process.env.DATABASE_URL;
|
||||||
@@ -14,6 +15,11 @@ if (!databaseUrl) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 定义输出目录
|
||||||
|
const DB_REPORTS_DIR = '/Users/liam/code/promote/backend/db-reports';
|
||||||
|
|
||||||
|
// 连接数据库
|
||||||
|
async function connect() {
|
||||||
console.log('使用PostgreSQL连接字符串连接数据库...');
|
console.log('使用PostgreSQL连接字符串连接数据库...');
|
||||||
|
|
||||||
// 创建PostgreSQL客户端
|
// 创建PostgreSQL客户端
|
||||||
@@ -21,8 +27,28 @@ const client = new Client({
|
|||||||
connectionString: databaseUrl,
|
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获取所有表...');
|
console.log('\n获取所有表...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -53,7 +79,7 @@ async function getAllTables() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取表结构
|
// 获取表结构
|
||||||
async function getTableSchema(tableName) {
|
async function getTableSchema(client, tableName) {
|
||||||
console.log(`\n获取表 ${tableName} 的结构...`);
|
console.log(`\n获取表 ${tableName} 的结构...`);
|
||||||
|
|
||||||
try {
|
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} 行)...`);
|
console.log(`\n获取表 ${tableName} 的数据示例 (最多 ${limit} 行)...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -126,40 +152,89 @@ async function getTableDataSample(tableName, limit = 5) {
|
|||||||
|
|
||||||
// 主函数
|
// 主函数
|
||||||
async function main() {
|
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 {
|
try {
|
||||||
// 连接数据库
|
// 连接数据库
|
||||||
await client.connect();
|
client = await connect();
|
||||||
console.log('成功连接到数据库');
|
|
||||||
|
|
||||||
// 获取所有表
|
// 获取所有表
|
||||||
const tables = await getAllTables();
|
const tables = await getAllTables(client);
|
||||||
|
|
||||||
if (!tables) {
|
if (!tables) {
|
||||||
console.error('无法获取表列表');
|
console.error('无法获取表列表');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取我们需要的表的结构和数据示例
|
console.log('\n所有PostgreSQL表:');
|
||||||
|
console.log(tables.join(', '));
|
||||||
|
|
||||||
|
// 获取特定表的结构,但不获取数据示例
|
||||||
const requiredTables = ['projects', 'influencers', 'project_influencers', 'posts'];
|
const requiredTables = ['projects', 'influencers', 'project_influencers', 'posts'];
|
||||||
|
|
||||||
for (const tableName of requiredTables) {
|
for (const tableName of requiredTables) {
|
||||||
if (tables.includes(tableName)) {
|
if (tables.includes(tableName)) {
|
||||||
await getTableSchema(tableName);
|
await getTableSchema(client, tableName);
|
||||||
await getTableDataSample(tableName);
|
// 移除数据示例检查
|
||||||
|
// await getTableDataSample(client, tableName);
|
||||||
} else {
|
} else {
|
||||||
console.log(`\n表 ${tableName} 不存在`);
|
console.log(`\n表 ${tableName} 不存在`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n数据库结构检查完成');
|
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) {
|
} catch (error) {
|
||||||
console.error('检查数据库结构时出错:', error);
|
console.error('检查数据库结构时出错:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} finally {
|
} finally {
|
||||||
|
// 恢复原始的console.log
|
||||||
|
console.log = originalConsoleLog;
|
||||||
|
|
||||||
// 关闭数据库连接
|
// 关闭数据库连接
|
||||||
await client.end();
|
if (client) {
|
||||||
|
await disconnect(client);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 运行主函数
|
// 导出函数
|
||||||
main();
|
module.exports = {
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
getAllTables,
|
||||||
|
getTableSchema,
|
||||||
|
getTableDataSample,
|
||||||
|
main
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果直接运行此脚本,则执行main函数
|
||||||
|
if (require.main === module) {
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('运行脚本时出错:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
102
backend/scripts/db-inspector/run-all.js
Normal file
102
backend/scripts/db-inspector/run-all.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
47
backend/scripts/db-inspector/run-all.sh
Executable file
47
backend/scripts/db-inspector/run-all.sh
Executable file
@@ -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"
|
||||||
@@ -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<string> {
|
|
||||||
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<string[]> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
Reference in New Issue
Block a user