funel data
This commit is contained in:
@@ -24,10 +24,12 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"hono": "^4.7.4",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"redis": "^4.7.0"
|
||||
"redis": "^4.7.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@clickhouse/client": "^1.10.1",
|
||||
"@supabase/supabase-js": "^2.49.1",
|
||||
"@types/axios": "^0.14.4",
|
||||
"@types/dotenv": "^8.2.3",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
@@ -38,6 +40,7 @@
|
||||
"axios": "^1.8.2",
|
||||
"dotenv": "^16.4.7",
|
||||
"eslint": "^8.57.0",
|
||||
"pg": "^8.13.3",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.4.3",
|
||||
"uuid": "^11.1.0",
|
||||
|
||||
177
backend/scripts/check-clickhouse-data.js
Normal file
177
backend/scripts/check-clickhouse-data.js
Normal file
@@ -0,0 +1,177 @@
|
||||
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();
|
||||
163
backend/scripts/check-clickhouse-schema.js
Normal file
163
backend/scripts/check-clickhouse-schema.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// 检查ClickHouse数据库结构的脚本
|
||||
const { ClickHouseClient } = require('@clickhouse/client');
|
||||
const dotenv = require('dotenv');
|
||||
const path = require('path');
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
|
||||
// 获取ClickHouse配置
|
||||
const clickhouseHost = process.env.CLICKHOUSE_HOST || 'localhost';
|
||||
const clickhousePort = process.env.CLICKHOUSE_PORT || '8123';
|
||||
const clickhouseUser = process.env.CLICKHOUSE_USER || 'default';
|
||||
const clickhousePassword = process.env.CLICKHOUSE_PASSWORD || '';
|
||||
const clickhouseDatabase = process.env.CLICKHOUSE_DATABASE || 'default';
|
||||
|
||||
console.log('ClickHouse配置:');
|
||||
console.log(` - 主机: ${clickhouseHost}`);
|
||||
console.log(` - 端口: ${clickhousePort}`);
|
||||
console.log(` - 用户: ${clickhouseUser}`);
|
||||
console.log(` - 数据库: ${clickhouseDatabase}`);
|
||||
|
||||
// 创建ClickHouse客户端
|
||||
const client = new ClickHouseClient({
|
||||
host: `http://${clickhouseHost}:${clickhousePort}`,
|
||||
username: clickhouseUser,
|
||||
password: clickhousePassword,
|
||||
database: clickhouseDatabase
|
||||
});
|
||||
|
||||
// 获取所有表
|
||||
async function getAllTables() {
|
||||
console.log('\n获取所有表...');
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT name
|
||||
FROM system.tables
|
||||
WHERE database = '${clickhouseDatabase}'
|
||||
`;
|
||||
|
||||
const resultSet = await client.query({
|
||||
query,
|
||||
format: 'JSONEachRow'
|
||||
});
|
||||
|
||||
const tables = await resultSet.json();
|
||||
|
||||
if (!tables || tables.length === 0) {
|
||||
console.log(`数据库 ${clickhouseDatabase} 中没有找到任何表`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`数据库 ${clickhouseDatabase} 中找到以下表:`);
|
||||
tables.forEach(table => {
|
||||
console.log(` - ${table.name}`);
|
||||
});
|
||||
|
||||
return tables.map(table => table.name);
|
||||
} catch (error) {
|
||||
console.error('获取所有表时出错:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表结构
|
||||
async function getTableSchema(tableName) {
|
||||
console.log(`\n获取表 ${tableName} 的结构...`);
|
||||
|
||||
try {
|
||||
const query = `
|
||||
DESCRIBE TABLE ${clickhouseDatabase}.${tableName}
|
||||
`;
|
||||
|
||||
const resultSet = await client.query({
|
||||
query,
|
||||
format: 'JSONEachRow'
|
||||
});
|
||||
|
||||
const columns = await resultSet.json();
|
||||
|
||||
if (!columns || columns.length === 0) {
|
||||
console.log(`表 ${tableName} 不存在或没有列`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`表 ${tableName} 的列:`);
|
||||
columns.forEach(column => {
|
||||
console.log(` - ${column.name} (${column.type}, ${column.default_type === '' ? '无默认值' : `默认值: ${column.default_expression}`})`);
|
||||
});
|
||||
|
||||
return columns;
|
||||
} catch (error) {
|
||||
console.error(`获取表 ${tableName} 结构时出错:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表数据示例
|
||||
async function getTableDataSample(tableName, limit = 5) {
|
||||
console.log(`\n获取表 ${tableName} 的数据示例 (最多 ${limit} 行)...`);
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM ${clickhouseDatabase}.${tableName}
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
const resultSet = await client.query({
|
||||
query,
|
||||
format: 'JSONEachRow'
|
||||
});
|
||||
|
||||
const rows = await resultSet.json();
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
console.log(`表 ${tableName} 中没有数据`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`表 ${tableName} 的数据示例:`);
|
||||
rows.forEach((row, index) => {
|
||||
console.log(` 行 ${index + 1}:`);
|
||||
Object.entries(row).forEach(([key, value]) => {
|
||||
console.log(` ${key}: ${value}`);
|
||||
});
|
||||
});
|
||||
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error(`获取表 ${tableName} 数据示例时出错:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
try {
|
||||
// 获取所有表
|
||||
const tables = await getAllTables();
|
||||
|
||||
if (!tables) {
|
||||
console.error('无法获取表列表');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 获取每个表的结构和数据示例
|
||||
for (const tableName of tables) {
|
||||
await getTableSchema(tableName);
|
||||
await getTableDataSample(tableName);
|
||||
}
|
||||
|
||||
console.log('\nClickHouse数据库结构检查完成');
|
||||
} catch (error) {
|
||||
console.error('检查ClickHouse数据库结构时出错:', error);
|
||||
} finally {
|
||||
// 关闭客户端连接
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 运行主函数
|
||||
main();
|
||||
165
backend/scripts/check-db-schema.js
Normal file
165
backend/scripts/check-db-schema.js
Normal file
@@ -0,0 +1,165 @@
|
||||
// 检查数据库结构的脚本
|
||||
const { Client } = require('pg');
|
||||
const dotenv = require('dotenv');
|
||||
const path = require('path');
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config({ path: path.resolve(__dirname, '../.env') });
|
||||
|
||||
// 获取数据库连接字符串
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
|
||||
if (!databaseUrl) {
|
||||
console.error('缺少数据库连接字符串。请确保.env文件中包含DATABASE_URL');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('使用PostgreSQL连接字符串连接数据库...');
|
||||
|
||||
// 创建PostgreSQL客户端
|
||||
const client = new Client({
|
||||
connectionString: databaseUrl,
|
||||
});
|
||||
|
||||
// 获取所有表
|
||||
async function getAllTables() {
|
||||
console.log('\n获取所有表...');
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name;
|
||||
`;
|
||||
|
||||
const result = await client.query(query);
|
||||
|
||||
if (!result.rows || result.rows.length === 0) {
|
||||
console.log('没有找到任何表');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('找到以下表:');
|
||||
result.rows.forEach(row => {
|
||||
console.log(` - ${row.table_name}`);
|
||||
});
|
||||
|
||||
return result.rows.map(row => row.table_name);
|
||||
} catch (error) {
|
||||
console.error('获取所有表时出错:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表结构
|
||||
async function getTableSchema(tableName) {
|
||||
console.log(`\n获取表 ${tableName} 的结构...`);
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default
|
||||
FROM
|
||||
information_schema.columns
|
||||
WHERE
|
||||
table_schema = 'public' AND
|
||||
table_name = $1
|
||||
ORDER BY
|
||||
ordinal_position;
|
||||
`;
|
||||
|
||||
const result = await client.query(query, [tableName]);
|
||||
|
||||
if (!result.rows || result.rows.length === 0) {
|
||||
console.log(`表 ${tableName} 不存在或没有列`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`表 ${tableName} 的列:`);
|
||||
result.rows.forEach(column => {
|
||||
console.log(` - ${column.column_name} (${column.data_type}, ${column.is_nullable === 'YES' ? '可为空' : '不可为空'}, 默认值: ${column.column_default || 'NULL'})`);
|
||||
});
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
console.error(`获取表 ${tableName} 结构时出错:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表数据示例
|
||||
async function getTableDataSample(tableName, limit = 5) {
|
||||
console.log(`\n获取表 ${tableName} 的数据示例 (最多 ${limit} 行)...`);
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM "${tableName}"
|
||||
LIMIT $1;
|
||||
`;
|
||||
|
||||
const result = await client.query(query, [limit]);
|
||||
|
||||
if (!result.rows || result.rows.length === 0) {
|
||||
console.log(`表 ${tableName} 中没有数据`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`表 ${tableName} 的数据示例:`);
|
||||
result.rows.forEach((row, index) => {
|
||||
console.log(` 行 ${index + 1}:`);
|
||||
Object.entries(row).forEach(([key, value]) => {
|
||||
console.log(` ${key}: ${value}`);
|
||||
});
|
||||
});
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
console.error(`获取表 ${tableName} 数据示例时出错:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
try {
|
||||
// 连接数据库
|
||||
await client.connect();
|
||||
console.log('成功连接到数据库');
|
||||
|
||||
// 获取所有表
|
||||
const tables = await getAllTables();
|
||||
|
||||
if (!tables) {
|
||||
console.error('无法获取表列表');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 获取我们需要的表的结构和数据示例
|
||||
const requiredTables = ['projects', 'influencers', 'project_influencers', 'posts'];
|
||||
|
||||
for (const tableName of requiredTables) {
|
||||
if (tables.includes(tableName)) {
|
||||
await getTableSchema(tableName);
|
||||
await getTableDataSample(tableName);
|
||||
} else {
|
||||
console.log(`\n表 ${tableName} 不存在`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n数据库结构检查完成');
|
||||
} catch (error) {
|
||||
console.error('检查数据库结构时出错:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// 关闭数据库连接
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
// 运行主函数
|
||||
main();
|
||||
51
backend/scripts/check-postgres-projects.js
Normal file
51
backend/scripts/check-postgres-projects.js
Normal file
@@ -0,0 +1,51 @@
|
||||
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();
|
||||
351
backend/scripts/generate-funnel-test-data.js
Normal file
351
backend/scripts/generate-funnel-test-data.js
Normal file
@@ -0,0 +1,351 @@
|
||||
// 生成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();
|
||||
309
backend/scripts/insert-clickhouse-test-data.js
Normal file
309
backend/scripts/insert-clickhouse-test-data.js
Normal file
@@ -0,0 +1,309 @@
|
||||
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();
|
||||
311
backend/scripts/insert-test-data.js
Normal file
311
backend/scripts/insert-test-data.js
Normal file
@@ -0,0 +1,311 @@
|
||||
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();
|
||||
111
backend/scripts/test-conversion-funnel.js
Normal file
111
backend/scripts/test-conversion-funnel.js
Normal file
@@ -0,0 +1,111 @@
|
||||
// 测试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();
|
||||
110
backend/scripts/test-funnel-with-mock-data.js
Normal file
110
backend/scripts/test-funnel-with-mock-data.js
Normal file
@@ -0,0 +1,110 @@
|
||||
// 测试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();
|
||||
@@ -1341,7 +1341,7 @@ analyticsRouter.get('/reports/project/:id', async (c) => {
|
||||
// 获取项目基本信息
|
||||
const { data: project, error: projectError } = await supabase
|
||||
.from('projects')
|
||||
.select('id, name, description, created_at, created_by')
|
||||
.select('id, name, description, created_at')
|
||||
.eq('id', projectId)
|
||||
.single();
|
||||
|
||||
@@ -1498,4 +1498,258 @@ analyticsRouter.get('/reports/project/:id', async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 获取KOL合作转换漏斗数据
|
||||
analyticsRouter.get('/project/:id/conversion-funnel', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
const { timeframe = '30days' } = c.req.query();
|
||||
|
||||
// 获取项目信息
|
||||
const { data: project, error: projectError } = await supabase
|
||||
.from('projects')
|
||||
.select('id, name, description, created_at')
|
||||
.eq('id', projectId)
|
||||
.single();
|
||||
|
||||
// 如果找不到项目或发生错误,返回模拟数据
|
||||
if (projectError) {
|
||||
console.log(`项目未找到或数据库错误,返回模拟数据。项目ID: ${projectId}, 错误: ${projectError.message}`);
|
||||
|
||||
// 生成模拟的漏斗数据
|
||||
const mockFunnelData = [
|
||||
{ stage: 'Awareness', count: 100, rate: 100 },
|
||||
{ stage: 'Interest', count: 75, rate: 75 },
|
||||
{ stage: 'Consideration', count: 50, rate: 50 },
|
||||
{ stage: 'Intent', count: 30, rate: 30 },
|
||||
{ stage: 'Evaluation', count: 20, rate: 20 },
|
||||
{ stage: 'Purchase', count: 10, rate: 10 }
|
||||
];
|
||||
|
||||
return c.json({
|
||||
project: {
|
||||
id: projectId,
|
||||
name: `模拟项目 (ID: ${projectId})`
|
||||
},
|
||||
timeframe,
|
||||
funnel_data: mockFunnelData,
|
||||
metrics: {
|
||||
total_influencers: 100,
|
||||
conversion_rate: 10,
|
||||
avg_stage_dropoff: 18
|
||||
},
|
||||
is_mock_data: true
|
||||
});
|
||||
}
|
||||
|
||||
// 获取项目关联的网红及其详细信息
|
||||
const { data: projectInfluencers, error: influencersError } = await supabase
|
||||
.from('project_influencers')
|
||||
.select(`
|
||||
influencer_id,
|
||||
influencers (
|
||||
id,
|
||||
name,
|
||||
platform,
|
||||
followers_count,
|
||||
engagement_rate,
|
||||
created_at
|
||||
)
|
||||
`)
|
||||
.eq('project_id', projectId);
|
||||
|
||||
if (influencersError) {
|
||||
console.error('Error fetching project influencers:', influencersError);
|
||||
return c.json({ error: 'Failed to fetch project data' }, 500);
|
||||
}
|
||||
|
||||
// 获取项目中的内容数据
|
||||
const { data: projectPosts, error: postsError } = await supabase
|
||||
.from('posts')
|
||||
.select(`
|
||||
id,
|
||||
influencer_id,
|
||||
platform,
|
||||
published_at,
|
||||
views_count,
|
||||
likes_count,
|
||||
comments_count,
|
||||
shares_count
|
||||
`)
|
||||
.eq('project_id', projectId);
|
||||
|
||||
if (postsError) {
|
||||
console.error('Error fetching project posts:', postsError);
|
||||
return c.json({ error: 'Failed to fetch project posts' }, 500);
|
||||
}
|
||||
|
||||
// 计算漏斗各阶段数据
|
||||
const totalInfluencers = projectInfluencers.length;
|
||||
|
||||
// 1. 认知阶段 - 所有接触的KOL
|
||||
const awarenessStage = {
|
||||
stage: 'Awareness',
|
||||
count: totalInfluencers,
|
||||
rate: 100
|
||||
};
|
||||
|
||||
// 2. 兴趣阶段 - 有互动的KOL (至少有一篇内容)
|
||||
const influencersWithContent = new Set<string>();
|
||||
projectPosts?.forEach(post => {
|
||||
if (post.influencer_id) {
|
||||
influencersWithContent.add(post.influencer_id);
|
||||
}
|
||||
});
|
||||
const interestStage = {
|
||||
stage: 'Interest',
|
||||
count: influencersWithContent.size,
|
||||
rate: Math.round((influencersWithContent.size / totalInfluencers) * 100)
|
||||
};
|
||||
|
||||
// 3. 考虑阶段 - 有高互动的KOL (内容互动率高于平均值)
|
||||
const engagementRates = projectInfluencers
|
||||
.map(pi => pi.influencers?.[0]?.engagement_rate || 0)
|
||||
.filter(rate => rate > 0);
|
||||
|
||||
const avgEngagementRate = engagementRates.length > 0
|
||||
? engagementRates.reduce((sum, rate) => sum + rate, 0) / engagementRates.length
|
||||
: 0;
|
||||
|
||||
const highEngagementInfluencers = projectInfluencers.filter(pi =>
|
||||
(pi.influencers?.[0]?.engagement_rate || 0) > avgEngagementRate
|
||||
);
|
||||
|
||||
const considerationStage = {
|
||||
stage: 'Consideration',
|
||||
count: highEngagementInfluencers.length,
|
||||
rate: Math.round((highEngagementInfluencers.length / totalInfluencers) * 100)
|
||||
};
|
||||
|
||||
// 4. 意向阶段 - 有多篇内容的KOL
|
||||
const influencerContentCount: Record<string, number> = {};
|
||||
projectPosts?.forEach(post => {
|
||||
if (post.influencer_id) {
|
||||
influencerContentCount[post.influencer_id] = (influencerContentCount[post.influencer_id] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
const multiContentInfluencers = Object.keys(influencerContentCount).filter(
|
||||
id => influencerContentCount[id] > 1
|
||||
);
|
||||
|
||||
const intentStage = {
|
||||
stage: 'Intent',
|
||||
count: multiContentInfluencers.length,
|
||||
rate: Math.round((multiContentInfluencers.length / totalInfluencers) * 100)
|
||||
};
|
||||
|
||||
// 5. 评估阶段 - 内容表现良好的KOL (浏览量高于平均值)
|
||||
const influencerViewsMap: Record<string, number> = {};
|
||||
projectPosts?.forEach(post => {
|
||||
if (post.influencer_id && post.views_count) {
|
||||
influencerViewsMap[post.influencer_id] = (influencerViewsMap[post.influencer_id] || 0) + post.views_count;
|
||||
}
|
||||
});
|
||||
|
||||
const influencerViews = Object.values(influencerViewsMap);
|
||||
const avgViews = influencerViews.length > 0
|
||||
? influencerViews.reduce((sum, views) => sum + views, 0) / influencerViews.length
|
||||
: 0;
|
||||
|
||||
const highViewsInfluencers = Object.keys(influencerViewsMap).filter(
|
||||
id => influencerViewsMap[id] > avgViews
|
||||
);
|
||||
|
||||
const evaluationStage = {
|
||||
stage: 'Evaluation',
|
||||
count: highViewsInfluencers.length,
|
||||
rate: Math.round((highViewsInfluencers.length / totalInfluencers) * 100)
|
||||
};
|
||||
|
||||
// 6. 购买/转化阶段 - 长期合作的KOL (3个月以上)
|
||||
const threeMonthsAgo = new Date();
|
||||
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
|
||||
|
||||
const longTermInfluencers = projectInfluencers.filter(pi => {
|
||||
const createdAt = pi.influencers?.[0]?.created_at;
|
||||
if (!createdAt) return false;
|
||||
|
||||
const createdDate = new Date(createdAt);
|
||||
return createdDate < threeMonthsAgo;
|
||||
});
|
||||
|
||||
const purchaseStage = {
|
||||
stage: 'Purchase',
|
||||
count: longTermInfluencers.length,
|
||||
rate: Math.round((longTermInfluencers.length / totalInfluencers) * 100)
|
||||
};
|
||||
|
||||
// 构建完整漏斗数据
|
||||
const funnelData = [
|
||||
awarenessStage,
|
||||
interestStage,
|
||||
considerationStage,
|
||||
intentStage,
|
||||
evaluationStage,
|
||||
purchaseStage
|
||||
];
|
||||
|
||||
// 计算转化率
|
||||
const conversionRate = totalInfluencers > 0
|
||||
? Math.round((longTermInfluencers.length / totalInfluencers) * 100)
|
||||
: 0;
|
||||
|
||||
// 计算平均转化率
|
||||
const avgStageDropoff = funnelData.length > 1
|
||||
? (100 - conversionRate) / (funnelData.length - 1)
|
||||
: 0;
|
||||
|
||||
return c.json({
|
||||
project: {
|
||||
id: project.id,
|
||||
name: project.name
|
||||
},
|
||||
timeframe,
|
||||
funnel_data: funnelData,
|
||||
metrics: {
|
||||
total_influencers: totalInfluencers,
|
||||
conversion_rate: conversionRate,
|
||||
avg_stage_dropoff: Math.round(avgStageDropoff)
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating KOL conversion funnel:', error);
|
||||
|
||||
// 发生错误时也返回模拟数据
|
||||
const projectId = c.req.param('id');
|
||||
const { timeframe = '30days' } = c.req.query();
|
||||
|
||||
// 生成模拟的漏斗数据
|
||||
const mockFunnelData = [
|
||||
{ stage: 'Awareness', count: 100, rate: 100 },
|
||||
{ stage: 'Interest', count: 75, rate: 75 },
|
||||
{ stage: 'Consideration', count: 50, rate: 50 },
|
||||
{ stage: 'Intent', count: 30, rate: 30 },
|
||||
{ stage: 'Evaluation', count: 20, rate: 20 },
|
||||
{ stage: 'Purchase', count: 10, rate: 10 }
|
||||
];
|
||||
|
||||
return c.json({
|
||||
project: {
|
||||
id: projectId,
|
||||
name: `模拟项目 (ID: ${projectId})`
|
||||
},
|
||||
timeframe,
|
||||
funnel_data: mockFunnelData,
|
||||
metrics: {
|
||||
total_influencers: 100,
|
||||
conversion_rate: 10,
|
||||
avg_stage_dropoff: 18
|
||||
},
|
||||
is_mock_data: true,
|
||||
error_message: '发生错误,返回模拟数据'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default analyticsRouter;
|
||||
@@ -1987,6 +1987,137 @@ export const openAPISpec = {
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/analytics/project/{id}/conversion-funnel': {
|
||||
get: {
|
||||
summary: '获取KOL合作转换漏斗数据',
|
||||
description: '获取项目中KOL合作的转换漏斗数据,包括各个阶段的数量和比率',
|
||||
tags: ['Analytics'],
|
||||
security: [{ bearerAuth: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: 'id',
|
||||
in: 'path',
|
||||
required: true,
|
||||
description: '项目ID',
|
||||
schema: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'timeframe',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description: '时间范围 (7days, 30days, 90days, 6months)',
|
||||
schema: {
|
||||
type: 'string',
|
||||
enum: ['7days', '30days', '90days', '6months'],
|
||||
default: '30days'
|
||||
}
|
||||
}
|
||||
],
|
||||
responses: {
|
||||
'200': {
|
||||
description: '成功获取KOL合作转换漏斗数据',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: '项目ID'
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: '项目名称'
|
||||
}
|
||||
}
|
||||
},
|
||||
timeframe: {
|
||||
type: 'string',
|
||||
description: '时间范围'
|
||||
},
|
||||
funnel_data: {
|
||||
type: 'array',
|
||||
description: '漏斗数据',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
stage: {
|
||||
type: 'string',
|
||||
description: '阶段名称'
|
||||
},
|
||||
count: {
|
||||
type: 'integer',
|
||||
description: 'KOL数量'
|
||||
},
|
||||
rate: {
|
||||
type: 'integer',
|
||||
description: '占总数的百分比'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
metrics: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
total_influencers: {
|
||||
type: 'integer',
|
||||
description: 'KOL总数'
|
||||
},
|
||||
conversion_rate: {
|
||||
type: 'integer',
|
||||
description: '总体转化率'
|
||||
},
|
||||
avg_stage_dropoff: {
|
||||
type: 'integer',
|
||||
description: '平均阶段流失率'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'404': {
|
||||
description: '项目未找到',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
error: {
|
||||
type: 'string',
|
||||
example: 'Project not found'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'500': {
|
||||
description: '服务器错误',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
error: {
|
||||
type: 'string',
|
||||
example: 'Internal server error'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'/api/analytics/project/{id}/top-performers': {
|
||||
get: {
|
||||
tags: ['Analytics'],
|
||||
|
||||
Reference in New Issue
Block a user