init ana page with apis

This commit is contained in:
2025-03-21 12:08:37 +08:00
commit 271230fca7
71 changed files with 15699 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
#!/bin/bash
set -e
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${YELLOW}开始检查ClickHouse表结构...${NC}"
# 加载环境变量
set -a
source .env
set +a
# 获取ClickHouse配置
CLICKHOUSE_HOST=${CLICKHOUSE_HOST:-"localhost"}
CLICKHOUSE_PORT=${CLICKHOUSE_PORT:-"8123"}
CLICKHOUSE_USER=${CLICKHOUSE_USER:-"default"}
CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-""}
CLICKHOUSE_DATABASE=${CLICKHOUSE_DATABASE:-"default"}
echo -e "${GREEN}连接到ClickHouse: ${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}${NC}"
# 检查link_events表结构
echo -e "${GREEN}检查link_events表结构:${NC}"
curl -s "http://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?user=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}" \
-d "DESCRIBE TABLE ${CLICKHOUSE_DATABASE}.link_events"
# 查询一行数据样本
echo -e "\n${GREEN}查询link_events表样本数据:${NC}"
curl -s "http://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?user=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}" \
-d "SELECT * FROM ${CLICKHOUSE_DATABASE}.link_events LIMIT 1 FORMAT JSON"
echo -e "\n${YELLOW}检查完成${NC}"

View File

@@ -0,0 +1,212 @@
// 检查ClickHouse数据库结构的脚本
const { createClient } = require('@clickhouse/client');
const dotenv = require('dotenv');
const path = require('path');
const fs = require('fs');
// 加载环境变量
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
// 定义输出目录
const DB_REPORTS_DIR = path.resolve(__dirname, '../db-reports');
// 获取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客户端 - 使用0.2.10版本的API
const client = createClient({
url: `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() {
let outputBuffer = '';
const originalConsoleLog = console.log;
// 重定向console.log到buffer和控制台
console.log = function() {
// 调用原始的console.log
originalConsoleLog.apply(console, arguments);
// 写入到buffer
outputBuffer += Array.from(arguments).join(' ') + '\n';
};
try {
// 获取所有表
const tables = await getAllTables();
if (!tables) {
console.error('无法获取表列表');
process.exit(1);
}
console.log('\n所有ClickHouse表:');
console.log(tables.join(', '));
// 获取每个表的结构,但不获取数据示例
for (const tableName of tables) {
await getTableSchema(tableName);
// 移除数据示例检查
// await getTableDataSample(tableName);
}
console.log('\nClickHouse数据库结构检查完成');
// 保存输出到指定目录
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
// 确保目录存在
if (!fs.existsSync(DB_REPORTS_DIR)) {
fs.mkdirSync(DB_REPORTS_DIR, { recursive: true });
}
const outputPath = path.join(DB_REPORTS_DIR, `clickhouse-schema-${timestamp}.log`);
fs.writeFileSync(outputPath, outputBuffer);
originalConsoleLog(`结果已保存到: ${outputPath}`);
} catch (error) {
console.error('检查ClickHouse数据库结构时出错:', error);
} finally {
// 恢复原始的console.log
console.log = originalConsoleLog;
// 关闭客户端连接
await client.close();
}
}
// 导出函数
module.exports = {
getAllTables,
getTableSchema,
getTableDataSample,
main
};
// 如果直接运行此脚本则执行main函数
if (require.main === module) {
main().catch(error => {
console.error('运行脚本时出错:', error);
process.exit(1);
});
}

View File

@@ -0,0 +1,329 @@
// 检查数据库结构的脚本
const { Client } = require('pg');
const dotenv = require('dotenv');
const path = require('path');
const fs = require('fs');
// 加载环境变量
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
// 获取数据库连接字符串
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error('缺少数据库连接字符串。请确保.env文件中包含DATABASE_URL');
process.exit(1);
}
// 定义输出目录
const DB_REPORTS_DIR = path.resolve(__dirname, '../db-reports');
// 连接数据库
async function connect() {
console.log('使用PostgreSQL连接字符串连接数据库...');
// 创建PostgreSQL客户端
const client = new Client({
connectionString: databaseUrl,
});
try {
await client.connect();
console.log('成功连接到数据库');
return client;
} catch (error) {
console.error('连接数据库失败:', error);
throw error;
}
}
// 断开数据库连接
async function disconnect(client) {
try {
await client.end();
console.log('已断开数据库连接');
} catch (error) {
console.error('断开数据库连接失败:', error);
}
}
// 获取所有表
async function getAllTables(client) {
console.log('\n获取所有表...');
try {
const query = `
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'limq'
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(client, tableName) {
console.log(`\n获取表 ${tableName} 的结构...`);
try {
// 获取基本列信息
const columnsQuery = `
SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length,
numeric_precision,
numeric_scale
FROM
information_schema.columns
WHERE
table_schema = 'limq' AND
table_name = $1
ORDER BY
ordinal_position;
`;
const columnsResult = await client.query(columnsQuery, [tableName]);
if (!columnsResult.rows || columnsResult.rows.length === 0) {
console.log(`${tableName} 不存在或没有列`);
return null;
}
// 获取主键信息
const primaryKeyQuery = `
SELECT
kcu.column_name
FROM
information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
WHERE
tc.constraint_type = 'PRIMARY KEY' AND
tc.table_schema = 'limq' AND
tc.table_name = $1
ORDER BY
kcu.ordinal_position;
`;
const primaryKeyResult = await client.query(primaryKeyQuery, [tableName]);
// 获取外键信息
const foreignKeysQuery = `
SELECT
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM
information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage ccu
ON tc.constraint_name = ccu.constraint_name
AND tc.table_schema = ccu.table_schema
WHERE
tc.constraint_type = 'FOREIGN KEY' AND
tc.table_schema = 'limq' AND
tc.table_name = $1;
`;
const foreignKeysResult = await client.query(foreignKeysQuery, [tableName]);
// 获取索引信息
const indexesQuery = `
SELECT
indexname,
indexdef
FROM
pg_indexes
WHERE
schemaname = 'public' AND
tablename = $1;
`;
const indexesResult = await client.query(indexesQuery, [tableName]);
// 输出列信息
console.log(`${tableName} 的列:`);
columnsResult.rows.forEach(column => {
console.log(` - ${column.column_name} (${column.data_type}${
column.character_maximum_length ? `(${column.character_maximum_length})` :
(column.numeric_precision ? `(${column.numeric_precision},${column.numeric_scale})` : '')
}, ${column.is_nullable === 'YES' ? '可为空' : '不可为空'}, 默认值: ${column.column_default || 'NULL'})`);
});
// 输出主键信息
if (primaryKeyResult.rows.length > 0) {
console.log(` 主键: ${primaryKeyResult.rows.map(row => row.column_name).join(', ')}`);
} else {
console.log(' 主键: 无');
}
// 输出外键信息
if (foreignKeysResult.rows.length > 0) {
console.log(' 外键:');
foreignKeysResult.rows.forEach(fk => {
console.log(` - ${fk.column_name} -> ${fk.foreign_table_name}.${fk.foreign_column_name}`);
});
} else {
console.log(' 外键: 无');
}
// 输出索引信息
if (indexesResult.rows.length > 0) {
console.log(' 索引:');
indexesResult.rows.forEach(idx => {
console.log(` - ${idx.indexname}: ${idx.indexdef}`);
});
} else {
console.log(' 索引: 无');
}
return {
columns: columnsResult.rows,
primaryKey: primaryKeyResult.rows,
foreignKeys: foreignKeysResult.rows,
indexes: indexesResult.rows
};
} catch (error) {
console.error(`获取表 ${tableName} 结构时出错:`, error);
return null;
}
}
// 获取表数据示例
async function getTableDataSample(client, 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() {
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 {
// 连接数据库
client = await connect();
// 获取所有表
const tables = await getAllTables(client);
if (!tables) {
console.error('无法获取表列表');
process.exit(1);
}
console.log('\n所有PostgreSQL表:');
console.log(tables.join(', '));
// 获取所有表的结构,而不只是特定表
for (const tableName of tables) {
await getTableSchema(client, tableName);
// 移除数据示例检查
// await getTableDataSample(client, tableName);
}
console.log('\n数据库结构检查完成');
// 保存输出到指定目录
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
// 确保目录存在
if (!fs.existsSync(DB_REPORTS_DIR)) {
fs.mkdirSync(DB_REPORTS_DIR, { recursive: true });
}
const outputPath = path.join(DB_REPORTS_DIR, `postgres-schema-${timestamp}.log`);
fs.writeFileSync(outputPath, outputBuffer);
originalConsoleLog(`结果已保存到: ${outputPath}`);
} catch (error) {
console.error('检查数据库结构时出错:', error);
process.exit(1);
} finally {
// 恢复原始的console.log
console.log = originalConsoleLog;
// 关闭数据库连接
if (client) {
await disconnect(client);
}
}
}
// 导出函数
module.exports = {
connect,
disconnect,
getAllTables,
getTableSchema,
getTableDataSample,
main
};
// 如果直接运行此脚本则执行main函数
if (require.main === module) {
main().catch(error => {
console.error('运行脚本时出错:', error);
process.exit(1);
});
}

View 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 = path.resolve(__dirname, '../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);
});
}

View File

@@ -0,0 +1,225 @@
获取所有表...
数据库 limq 中找到以下表:
- .inner_id.5d9e5f95-ad7d-4750-ae56-bffea63e14fb
- .inner_id.711eb652-7c90-4f9a-80a0-8979011080cc
- .inner_id.abec445d-1704-4482-bc72-66c9eb67ecd1
- .inner_id.c1eb844d-7f11-4cfc-8931-c433faaa16b0
- .inner_id.f9640e70-5b7f-444c-80de-bc5b25848024
- .inner_id.fe81eeba-acc5-4260-ac9a-973c2f9ce1ea
- link_daily_stats
- link_events
- link_hourly_patterns
- links
- platform_distribution
- project_daily_stats
- projects
- qr_scans
- qrcode_daily_stats
- qrcodes
- sessions
- team_daily_stats
- team_members
- teams
所有ClickHouse表:
.inner_id.5d9e5f95-ad7d-4750-ae56-bffea63e14fb, .inner_id.711eb652-7c90-4f9a-80a0-8979011080cc, .inner_id.abec445d-1704-4482-bc72-66c9eb67ecd1, .inner_id.c1eb844d-7f11-4cfc-8931-c433faaa16b0, .inner_id.f9640e70-5b7f-444c-80de-bc5b25848024, .inner_id.fe81eeba-acc5-4260-ac9a-973c2f9ce1ea, link_daily_stats, link_events, link_hourly_patterns, links, platform_distribution, project_daily_stats, projects, qr_scans, qrcode_daily_stats, qrcodes, sessions, team_daily_stats, team_members, teams
获取表 .inner_id.5d9e5f95-ad7d-4750-ae56-bffea63e14fb 的结构...
获取表 .inner_id.711eb652-7c90-4f9a-80a0-8979011080cc 的结构...
获取表 .inner_id.abec445d-1704-4482-bc72-66c9eb67ecd1 的结构...
获取表 .inner_id.c1eb844d-7f11-4cfc-8931-c433faaa16b0 的结构...
获取表 .inner_id.f9640e70-5b7f-444c-80de-bc5b25848024 的结构...
获取表 .inner_id.fe81eeba-acc5-4260-ac9a-973c2f9ce1ea 的结构...
获取表 link_daily_stats 的结构...
表 link_daily_stats 的列:
- date (Date, 无默认值)
- link_id (String, 无默认值)
- total_clicks (UInt64, 无默认值)
- unique_visitors (UInt64, 无默认值)
- unique_sessions (UInt64, 无默认值)
- total_time_spent (UInt64, 无默认值)
- avg_time_spent (Float64, 无默认值)
- bounce_count (UInt64, 无默认值)
- conversion_count (UInt64, 无默认值)
- unique_referrers (UInt64, 无默认值)
- mobile_count (UInt64, 无默认值)
- tablet_count (UInt64, 无默认值)
- desktop_count (UInt64, 无默认值)
- qr_scan_count (UInt64, 无默认值)
- total_conversion_value (Float64, 无默认值)
获取表 link_events 的结构...
表 link_events 的列:
- event_id (UUID, 默认值: generateUUIDv4())
- event_time (DateTime64(3), 默认值: now64())
- date (Date, 默认值: toDate(event_time))
- link_id (String, 无默认值)
- channel_id (String, 无默认值)
- visitor_id (String, 无默认值)
- session_id (String, 无默认值)
- event_type (Enum8('click' = 1, 'redirect' = 2, 'conversion' = 3, 'error' = 4), 无默认值)
- ip_address (String, 无默认值)
- country (String, 无默认值)
- city (String, 无默认值)
- referrer (String, 无默认值)
- utm_source (String, 无默认值)
- utm_medium (String, 无默认值)
- utm_campaign (String, 无默认值)
- user_agent (String, 无默认值)
- device_type (Enum8('mobile' = 1, 'tablet' = 2, 'desktop' = 3, 'other' = 4), 无默认值)
- browser (String, 无默认值)
- os (String, 无默认值)
- time_spent_sec (UInt32, 默认值: 0)
- is_bounce (Bool, 默认值: true)
- is_qr_scan (Bool, 默认值: false)
- qr_code_id (String, 默认值: '')
- conversion_type (Enum8('visit' = 1, 'stay' = 2, 'interact' = 3, 'signup' = 4, 'subscription' = 5, 'purchase' = 6), 默认值: 'visit')
- conversion_value (Float64, 默认值: 0)
- custom_data (String, 默认值: '{}')
获取表 link_hourly_patterns 的结构...
表 link_hourly_patterns 的列:
- date (Date, 无默认值)
- hour (UInt8, 无默认值)
- link_id (String, 无默认值)
- visits (UInt64, 无默认值)
- unique_visitors (UInt64, 无默认值)
获取表 links 的结构...
表 links 的列:
- link_id (String, 无默认值)
- original_url (String, 无默认值)
- created_at (DateTime64(3), 无默认值)
- created_by (String, 无默认值)
- title (String, 无默认值)
- description (String, 无默认值)
- tags (Array(String), 无默认值)
- is_active (Bool, 默认值: true)
- expires_at (Nullable(DateTime), 无默认值)
- team_id (String, 默认值: '')
- project_id (String, 默认值: '')
获取表 platform_distribution 的结构...
表 platform_distribution 的列:
- date (Date, 无默认值)
- utm_source (String, 无默认值)
- device_type (Enum8('mobile' = 1, 'tablet' = 2, 'desktop' = 3, 'other' = 4), 无默认值)
- visits (UInt64, 无默认值)
- unique_visitors (UInt64, 无默认值)
获取表 project_daily_stats 的结构...
表 project_daily_stats 的列:
- date (Date, 无默认值)
- project_id (String, 无默认值)
- total_clicks (UInt64, 无默认值)
- unique_visitors (UInt64, 无默认值)
- conversion_count (UInt64, 无默认值)
- links_used (UInt64, 无默认值)
- qr_scan_count (UInt64, 无默认值)
获取表 projects 的结构...
表 projects 的列:
- project_id (String, 无默认值)
- team_id (String, 无默认值)
- name (String, 无默认值)
- created_at (DateTime, 无默认值)
- created_by (String, 无默认值)
- description (String, 默认值: '')
- is_archived (Bool, 默认值: false)
- links_count (UInt32, 默认值: 0)
- total_clicks (UInt64, 默认值: 0)
- last_updated (DateTime, 默认值: now())
获取表 qr_scans 的结构...
表 qr_scans 的列:
- scan_id (UUID, 默认值: generateUUIDv4())
- qr_code_id (String, 无默认值)
- link_id (String, 无默认值)
- scan_time (DateTime64(3), 无默认值)
- visitor_id (String, 无默认值)
- location (String, 无默认值)
- device_type (Enum8('mobile' = 1, 'tablet' = 2, 'desktop' = 3, 'other' = 4), 无默认值)
- led_to_conversion (Bool, 默认值: false)
获取表 qrcode_daily_stats 的结构...
表 qrcode_daily_stats 的列:
- date (Date, 无默认值)
- qr_code_id (String, 无默认值)
- total_scans (UInt64, 无默认值)
- unique_scanners (UInt64, 无默认值)
- conversions (UInt64, 无默认值)
- mobile_scans (UInt64, 无默认值)
- tablet_scans (UInt64, 无默认值)
- desktop_scans (UInt64, 无默认值)
- unique_locations (UInt64, 无默认值)
获取表 qrcodes 的结构...
表 qrcodes 的列:
- qr_code_id (String, 无默认值)
- link_id (String, 无默认值)
- team_id (String, 无默认值)
- project_id (String, 默认值: '')
- name (String, 无默认值)
- description (String, 默认值: '')
- created_at (DateTime, 无默认值)
- created_by (String, 无默认值)
- updated_at (DateTime, 默认值: now())
- qr_type (Enum8('standard' = 1, 'custom' = 2, 'dynamic' = 3), 默认值: 'standard')
- image_url (String, 默认值: '')
- design_config (String, 默认值: '{}')
- is_active (Bool, 默认值: true)
- total_scans (UInt64, 默认值: 0)
- unique_scanners (UInt32, 默认值: 0)
获取表 sessions 的结构...
表 sessions 的列:
- session_id (String, 无默认值)
- visitor_id (String, 无默认值)
- link_id (String, 无默认值)
- started_at (DateTime64(3), 无默认值)
- last_activity (DateTime64(3), 无默认值)
- ended_at (Nullable(DateTime64(3)), 无默认值)
- duration_sec (UInt32, 默认值: 0)
- session_pages (UInt8, 默认值: 1)
- is_completed (Bool, 默认值: false)
获取表 team_daily_stats 的结构...
表 team_daily_stats 的列:
- date (Date, 无默认值)
- team_id (String, 无默认值)
- total_clicks (UInt64, 无默认值)
- unique_visitors (UInt64, 无默认值)
- conversion_count (UInt64, 无默认值)
- links_used (UInt64, 无默认值)
- qr_scan_count (UInt64, 无默认值)
获取表 team_members 的结构...
表 team_members 的列:
- team_id (String, 无默认值)
- user_id (String, 无默认值)
- role (Enum8('owner' = 1, 'admin' = 2, 'editor' = 3, 'viewer' = 4), 无默认值)
- joined_at (DateTime, 默认值: now())
- invited_by (String, 无默认值)
- is_active (Bool, 默认值: true)
- last_active (DateTime, 默认值: now())
获取表 teams 的结构...
表 teams 的列:
- team_id (String, 无默认值)
- name (String, 无默认值)
- created_at (DateTime, 无默认值)
- created_by (String, 无默认值)
- description (String, 默认值: '')
- avatar_url (String, 默认值: '')
- is_active (Bool, 默认值: true)
- plan_type (Enum8('free' = 1, 'pro' = 2, 'enterprise' = 3), 无默认值)
- members_count (UInt32, 默认值: 1)
ClickHouse数据库结构检查完成

View File

@@ -0,0 +1,483 @@
使用PostgreSQL连接字符串连接数据库...
成功连接到数据库
获取所有表...
找到以下表:
- ProjectTeams
- attribute_schemas
- channel
- channel_tag
- favorite
- form_field_metadata
- google_token
- materials
- permission_resources
- permissions
- platform_tokens
- project_resources
- projects
- qr_code
- queue_tasks
- resource_tags
- resources
- roles
- slide_presentations
- subscription
- sync_resource_typesense_queue
- tags
- team_invitation
- team_join_request
- team_membership
- team_projects
- teams
- type_order
- user_projects
- users
所有PostgreSQL表:
ProjectTeams, attribute_schemas, channel, channel_tag, favorite, form_field_metadata, google_token, materials, permission_resources, permissions, platform_tokens, project_resources, projects, qr_code, queue_tasks, resource_tags, resources, roles, slide_presentations, subscription, sync_resource_typesense_queue, tags, team_invitation, team_join_request, team_membership, team_projects, teams, type_order, user_projects, users
获取表 ProjectTeams 的结构...
表 ProjectTeams 的列:
- A (uuid, 不可为空, 默认值: NULL)
- B (uuid, 不可为空, 默认值: NULL)
主键: 无
外键:
- A -> projects.id
索引: 无
获取表 attribute_schemas 的结构...
表 attribute_schemas 的列:
- version (integer(32,0), 可为空, 默认值: NULL)
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- entity_type (text, 可为空, 默认值: NULL)
- schema (jsonb, 可为空, 默认值: NULL)
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- is_active (boolean, 可为空, 默认值: NULL)
主键: id
外键: 无
索引: 无
获取表 channel 的结构...
表 channel 的列:
- name (text, 不可为空, 默认值: NULL)
- path (text, 不可为空, 默认值: NULL)
- shortUrlId (uuid, 可为空, 默认值: NULL)
- qrCodeId (uuid, 可为空, 默认值: NULL)
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- isUserCreated (boolean, 可为空, 默认值: false)
主键: id
外键:
- qrCodeId -> qr_code.id
- shortUrlId -> resources.id
索引: 无
获取表 channel_tag 的结构...
表 channel_tag 的列:
- channelId (uuid, 不可为空, 默认值: NULL)
- tagId (uuid, 不可为空, 默认值: NULL)
- id (uuid, 不可为空, 默认值: gen_random_uuid())
主键: id
外键:
- channelId -> channel.id
- tagId -> tags.id
索引: 无
获取表 favorite 的结构...
表 favorite 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- user_id (uuid, 不可为空, 默认值: NULL)
- favoritable_id (uuid, 不可为空, 默认值: NULL)
- favoritable_type (text, 不可为空, 默认值: NULL)
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
主键: id
外键: 无
索引: 无
获取表 form_field_metadata 的结构...
表 form_field_metadata 的列:
- field_name (text, 可为空, 默认值: NULL)
- resource_type (text, 可为空, 默认值: NULL)
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- default_value (jsonb, 可为空, 默认值: NULL)
- options (jsonb, 可为空, 默认值: NULL)
- validation_rules (jsonb, 可为空, 默认值: NULL)
- is_required (boolean, 可为空, 默认值: NULL)
- order_index (integer(32,0), 可为空, 默认值: NULL)
- is_visible (boolean, 可为空, 默认值: NULL)
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- placeholder (text, 可为空, 默认值: NULL)
- label (text, 可为空, 默认值: NULL)
- field_type (text, 可为空, 默认值: NULL)
主键: id
外键: 无
索引: 无
获取表 google_token 的结构...
表 google_token 的列:
- id (text, 不可为空, 默认值: NULL)
- googleEmail (text, 不可为空, 默认值: NULL)
- created_at (timestamp without time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp without time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
- profile_data (jsonb, 可为空, 默认值: NULL)
- tokens (jsonb, 不可为空, 默认值: NULL)
- user_id (uuid, 不可为空, 默认值: NULL)
主键: id
外键:
- user_id -> users.id
索引: 无
获取表 materials 的结构...
表 materials 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- name (text, 不可为空, 默认值: NULL)
- description (text, 可为空, 默认值: ''::text)
- attributes (jsonb, 可为空, 默认值: '{}'::jsonb)
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
- schema_version (integer(32,0), 可为空, 默认值: 1)
- type (text, 不可为空, 默认值: NULL)
- is_active (boolean, 可为空, 默认值: true)
- is_system (boolean, 可为空, 默认值: false)
- team_id (uuid, 可为空, 默认值: NULL)
- creator_id (uuid, 可为空, 默认值: NULL)
主键: id
外键: 无
索引: 无
获取表 permission_resources 的结构...
表 permission_resources 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- created_at (timestamp with time zone, 可为空, 默认值: now())
- updated_at (timestamp with time zone, 可为空, 默认值: now())
- resource_type (USER-DEFINED, 可为空, 默认值: NULL)
- resource_name (text, 不可为空, 默认值: NULL)
- attributes_type (text, 可为空, 默认值: NULL)
- description (text, 可为空, 默认值: NULL)
主键: id
外键: 无
索引: 无
获取表 permissions 的结构...
表 permissions 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- created_at (timestamp with time zone, 可为空, 默认值: now())
- updated_at (timestamp with time zone, 可为空, 默认值: now())
- role_id (uuid, 不可为空, 默认值: NULL)
- resource_id (uuid, 不可为空, 默认值: NULL)
- action (USER-DEFINED, 不可为空, 默认值: NULL)
- metadata (jsonb, 可为空, 默认值: NULL)
主键: id
外键:
- resource_id -> permission_resources.id
- role_id -> roles.id
索引: 无
获取表 platform_tokens 的结构...
表 platform_tokens 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- user_id (uuid, 不可为空, 默认值: NULL)
- platform (text, 不可为空, 默认值: NULL)
- access_token (text, 不可为空, 默认值: NULL)
- refresh_token (text, 可为空, 默认值: NULL)
- expires_at (timestamp with time zone, 可为空, 默认值: NULL)
- token_type (text, 可为空, 默认值: NULL)
- scope (text, 可为空, 默认值: NULL)
- created_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
- metadata (jsonb, 可为空, 默认值: NULL)
主键: id
外键:
- user_id -> users.id
索引: 无
获取表 project_resources 的结构...
表 project_resources 的列:
- project_id (uuid, 不可为空, 默认值: NULL)
- resource_id (uuid, 不可为空, 默认值: NULL)
- assigned_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
主键: project_id, resource_id
外键:
- project_id -> projects.id
- resource_id -> resources.id
索引: 无
获取表 projects 的结构...
表 projects 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- name (text, 不可为空, 默认值: NULL)
- description (text, 可为空, 默认值: NULL)
- attributes (jsonb, 可为空, 默认值: NULL)
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
- schema_version (integer(32,0), 可为空, 默认值: NULL)
- creator_id (uuid, 可为空, 默认值: NULL)
主键: id
外键: 无
索引: 无
获取表 qr_code 的结构...
表 qr_code 的列:
- scan_count (integer(32,0), 不可为空, 默认值: 0)
- url (text, 不可为空, 默认值: NULL)
- options (jsonb, 可为空, 默认值: NULL)
- template_id (text, 可为空, 默认值: NULL)
- msg_template_id (text, 可为空, 默认值: NULL)
- template_name (text, 可为空, 默认值: NULL)
- attributes (jsonb, 可为空, 默认值: NULL)
- created_at (timestamp without time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp without time zone, 不可为空, 默认值: NULL)
- deleted_at (timestamp without time zone, 可为空, 默认值: NULL)
- resource_id (uuid, 可为空, 默认值: NULL)
- id (uuid, 不可为空, 默认值: gen_random_uuid())
主键: id
外键:
- resource_id -> resources.id
索引: 无
获取表 queue_tasks 的结构...
表 queue_tasks 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- task_id (text, 不可为空, 默认值: NULL)
- type (text, 不可为空, 默认值: NULL)
- status (text, 不可为空, 默认值: NULL)
- data (jsonb, 可为空, 默认值: NULL)
- result (jsonb, 可为空, 默认值: NULL)
- error (text, 可为空, 默认值: NULL)
- created_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
- started_at (timestamp with time zone, 可为空, 默认值: NULL)
- finished_at (timestamp with time zone, 可为空, 默认值: NULL)
- creator_id (uuid, 可为空, 默认值: NULL)
主键: id
外键:
- creator_id -> users.id
索引: 无
获取表 resource_tags 的结构...
表 resource_tags 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- tag_id (uuid, 不可为空, 默认值: NULL)
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- resource_id (uuid, 不可为空, 默认值: NULL)
主键: id
外键:
- resource_id -> resources.id
- tag_id -> tags.id
索引: 无
获取表 resources 的结构...
表 resources 的列:
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
- schema_version (integer(32,0), 可为空, 默认值: NULL)
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- external_id (text, 可为空, 默认值: NULL)
- attributes (jsonb, 可为空, 默认值: NULL)
- type (text, 可为空, 默认值: NULL)
- creator_id (uuid, 可为空, 默认值: NULL)
主键: id
外键:
- creator_id -> users.id
索引: 无
获取表 roles 的结构...
表 roles 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- created_at (timestamp with time zone, 可为空, 默认值: now())
- updated_at (timestamp with time zone, 可为空, 默认值: now())
- name (text, 不可为空, 默认值: NULL)
- description (text, 可为空, 默认值: NULL)
主键: id
外键: 无
索引: 无
获取表 slide_presentations 的结构...
表 slide_presentations 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- slide_id (text, 不可为空, 默认值: NULL)
- user_id (uuid, 不可为空, 默认值: NULL)
- content (jsonb, 不可为空, 默认值: NULL)
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
主键: id
外键:
- user_id -> users.id
索引: 无
获取表 subscription 的结构...
表 subscription 的列:
- id (text, 不可为空, 默认值: NULL)
- team_id (uuid, 不可为空, 默认值: NULL)
- customer_id (text, 不可为空, 默认值: NULL)
- status (USER-DEFINED, 不可为空, 默认值: NULL)
- plan_id (text, 不可为空, 默认值: NULL)
- variant_id (text, 不可为空, 默认值: NULL)
- next_payment_date (timestamp without time zone, 可为空, 默认值: NULL)
主键: id
外键: 无
索引: 无
获取表 sync_resource_typesense_queue 的结构...
表 sync_resource_typesense_queue 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- resource_id (uuid, 不可为空, 默认值: NULL)
- entity_type (text, 不可为空, 默认值: NULL)
- action (text, 不可为空, 默认值: NULL)
- status (text, 不可为空, 默认值: NULL)
- error (text, 可为空, 默认值: NULL)
- created_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
- processed_at (timestamp with time zone, 可为空, 默认值: NULL)
主键: id
外键:
- resource_id -> resources.id
索引: 无
获取表 tags 的结构...
表 tags 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- name (text, 可为空, 默认值: NULL)
- type (text, 可为空, 默认值: NULL)
- attributes (jsonb, 可为空, 默认值: NULL)
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
- parent_tag_id (uuid, 可为空, 默认值: NULL)
- team_id (uuid, 可为空, 默认值: NULL)
- is_shared (boolean, 可为空, 默认值: false)
- schema_version (integer(32,0), 可为空, 默认值: NULL)
- is_system (boolean, 可为空, 默认值: false)
主键: id
外键:
- parent_tag_id -> tags.id
索引: 无
获取表 team_invitation 的结构...
表 team_invitation 的列:
- id (text, 不可为空, 默认值: NULL)
- team_id (uuid, 不可为空, 默认值: NULL)
- email (text, 可为空, 默认值: NULL)
- role (USER-DEFINED, 不可为空, 默认值: 'MEMBER'::limq.team_member_role)
- created_at (timestamp without time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
- expires_at (timestamp without time zone, 不可为空, 默认值: NULL)
- type (text, 可为空, 默认值: NULL)
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
- status (USER-DEFINED, 可为空, 默认值: NULL)
主键: id
外键:
- team_id -> teams.id
索引: 无
获取表 team_join_request 的结构...
表 team_join_request 的列:
- id (text, 不可为空, 默认值: NULL)
- team_id (uuid, 不可为空, 默认值: NULL)
- user_id (uuid, 不可为空, 默认值: NULL)
- invitation_id (text, 不可为空, 默认值: NULL)
- status (USER-DEFINED, 不可为空, 默认值: 'PENDING'::limq.request_status)
- message (text, 可为空, 默认值: NULL)
- created_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 不可为空, 默认值: NULL)
主键: id
外键:
- invitation_id -> team_invitation.id
- user_id -> users.id
索引: 无
获取表 team_membership 的结构...
表 team_membership 的列:
- id (text, 不可为空, 默认值: NULL)
- team_id (uuid, 不可为空, 默认值: NULL)
- user_id (uuid, 不可为空, 默认值: NULL)
- is_creator (boolean, 不可为空, 默认值: false)
- role (text, 不可为空, 默认值: NULL)
主键: id
外键:
- team_id -> teams.id
- user_id -> users.id
索引: 无
获取表 team_projects 的结构...
表 team_projects 的列:
- team_id (uuid, 不可为空, 默认值: NULL)
- project_id (uuid, 不可为空, 默认值: NULL)
- assigned_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
主键: team_id, project_id
外键:
- project_id -> projects.id
- team_id -> teams.id
索引: 无
获取表 teams 的结构...
表 teams 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- name (text, 不可为空, 默认值: NULL)
- description (text, 可为空, 默认值: NULL)
- attributes (jsonb, 可为空, 默认值: NULL)
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
- schema_version (integer(32,0), 可为空, 默认值: NULL)
- avatar_url (text, 可为空, 默认值: NULL)
主键: id
外键: 无
索引: 无
获取表 type_order 的结构...
表 type_order 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- user_id (uuid, 不可为空, 默认值: NULL)
- category (text, 不可为空, 默认值: NULL)
- type (text, 不可为空, 默认值: NULL)
- order (integer(32,0), 不可为空, 默认值: NULL)
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
主键: id
外键:
- user_id -> users.id
索引: 无
获取表 user_projects 的结构...
表 user_projects 的列:
- user_id (uuid, 不可为空, 默认值: NULL)
- project_id (uuid, 不可为空, 默认值: NULL)
- role (text, 可为空, 默认值: NULL)
- joined_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
主键: user_id, project_id
外键:
- project_id -> projects.id
- user_id -> users.id
索引: 无
获取表 users 的结构...
表 users 的列:
- id (uuid, 不可为空, 默认值: gen_random_uuid())
- email (text, 不可为空, 默认值: NULL)
- password_hash (text, 可为空, 默认值: NULL)
- first_name (text, 可为空, 默认值: NULL)
- last_name (text, 可为空, 默认值: NULL)
- phone_number (text, 可为空, 默认值: NULL)
- is_email_verified (boolean, 可为空, 默认值: NULL)
- is_phone_verified (boolean, 可为空, 默认值: NULL)
- last_login_at (timestamp with time zone, 可为空, 默认值: NULL)
- attributes (jsonb, 可为空, 默认值: NULL)
- external_id (text, 可为空, 默认值: NULL)
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
- schema_version (integer(32,0), 可为空, 默认值: NULL)
- avatar_url (text, 可为空, 默认值: NULL)
主键: id
外键: 无
索引: 无
数据库结构检查完成

View File

@@ -0,0 +1,29 @@
#!/bin/bash
# 脚本名称: load-clickhouse-testdata.sh
# 用途: 将测试数据加载到ClickHouse数据库中
# 设置脚本目录路径
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 设置SQL文件路径
SQL_FILE="$SCRIPT_DIR/sql/clickhouse/seed-clickhouse-analytics.sql"
# 检查SQL文件是否存在
if [ ! -f "$SQL_FILE" ]; then
echo "错误: SQL文件 '$SQL_FILE' 不存在"
exit 1
fi
# 执行CH查询脚本
echo "开始加载测试数据到ClickHouse数据库..."
bash "$SCRIPT_DIR/sql/clickhouse/ch-query.sh" -f "$SQL_FILE"
# 检查执行结果
if [ $? -eq 0 ]; then
echo "测试数据已成功加载到ClickHouse数据库"
else
echo "错误: 加载测试数据失败"
exit 1
fi
exit 0

View File

@@ -0,0 +1,102 @@
#!/bin/bash
# 文件名: ch-query.sh
# 用途: 执行ClickHouse SQL查询的便捷脚本
# 连接参数
CH_HOST="localhost"
CH_PORT="9000"
CH_USER="admin"
CH_PASSWORD="your_secure_password"
# 基本查询函数
function ch_query() {
clickhouse client --host $CH_HOST --port $CH_PORT --user $CH_USER --password $CH_PASSWORD -q "$1"
}
# 显示帮助信息
function show_help() {
echo "ClickHouse 查询工具"
echo "用法: $0 [选项] [SQL查询]"
echo ""
echo "选项:"
echo " -t 显示所有表"
echo " -d 显示所有数据库"
echo " -s <表名> 显示表结构"
echo " -p <表名> 显示表样本数据(前10行)"
echo " -c <表名> 计算表中的记录数"
echo " -h, --help 显示此帮助信息"
echo " -q \"SQL查询\" 执行自定义SQL查询"
echo " -f <文件名> 执行SQL文件"
echo ""
echo "示例:"
echo " $0 -d # 显示所有数据库"
echo " $0 -t # 显示所有表"
echo " $0 -s limq.link_events # 显示link_events表结构"
echo " $0 -q \"SELECT * FROM limq.link_events LIMIT 5\" # 执行自定义查询"
}
# 没有参数时显示帮助
if [ $# -eq 0 ]; then
show_help
exit 0
fi
# 处理命令行参数
case "$1" in
-t)
ch_query "SHOW TABLES"
;;
-d)
ch_query "SHOW DATABASES"
;;
-s)
if [ -z "$2" ]; then
echo "错误: 需要提供表名"
exit 1
fi
ch_query "DESCRIBE TABLE $2"
;;
-p)
if [ -z "$2" ]; then
echo "错误: 需要提供表名"
exit 1
fi
ch_query "SELECT * FROM $2 LIMIT 10"
;;
-c)
if [ -z "$2" ]; then
echo "错误: 需要提供表名"
exit 1
fi
ch_query "SELECT COUNT(*) FROM $2"
;;
-q)
if [ -z "$2" ]; then
echo "错误: 需要提供SQL查询"
exit 1
fi
ch_query "$2"
;;
-f)
if [ -z "$2" ]; then
echo "错误: 需要提供SQL文件"
exit 1
fi
if [ ! -f "$2" ]; then
echo "错误: 文件 '$2' 不存在"
exit 1
fi
SQL=$(cat "$2")
ch_query "$SQL"
;;
-h|--help)
show_help
;;
*)
echo "未知选项: $1"
show_help
exit 1
;;
esac
exit 0

View File

@@ -0,0 +1,4 @@
```bash
alias clickhouse-sql='clickhouse client --host localhost --port 9000 --user admin --password your_secure_password --database promote -q'
clickhouse-sql "SHOW TABLES"
```

View File

@@ -0,0 +1,170 @@
-- 创建数据库
CREATE DATABASE IF NOT EXISTS limq;
-- 切换到limq数据库
USE limq;
-- 创建短链接访问事件表
CREATE TABLE IF NOT EXISTS limq.link_events (
event_id UUID DEFAULT generateUUIDv4(),
event_time DateTime64(3) DEFAULT now64(),
date Date DEFAULT toDate(event_time),
link_id String,
channel_id String,
visitor_id String,
session_id String,
event_type Enum8(
'click' = 1,
'redirect' = 2,
'conversion' = 3,
'error' = 4
),
-- 访问者信息
ip_address String,
country String,
city String,
-- 来源信息
referrer String,
utm_source String,
utm_medium String,
utm_campaign String,
-- 设备信息
user_agent String,
device_type Enum8(
'mobile' = 1,
'tablet' = 2,
'desktop' = 3,
'other' = 4
),
browser String,
os String,
-- 交互信息
time_spent_sec UInt32 DEFAULT 0,
is_bounce Boolean DEFAULT true,
-- QR码相关
is_qr_scan Boolean DEFAULT false,
qr_code_id String DEFAULT '',
-- 转化数据
conversion_type String DEFAULT '',
conversion_value Float64 DEFAULT 0,
-- 其他属性
custom_data String DEFAULT '{}'
) ENGINE = MergeTree() PARTITION BY toYYYYMM(date)
ORDER BY
(date, link_id, event_time) SETTINGS index_granularity = 8192;
-- 短链接维度表
CREATE TABLE IF NOT EXISTS limq.links (
link_id String,
original_url String,
created_at DateTime,
created_by String,
title String,
description String,
tags Array(String),
is_active Boolean DEFAULT true,
expires_at Nullable(DateTime),
team_id String DEFAULT '',
project_id String DEFAULT '',
PRIMARY KEY (link_id)
) ENGINE = ReplacingMergeTree()
ORDER BY
link_id SETTINGS index_granularity = 8192;
-- 会话跟踪表
CREATE TABLE IF NOT EXISTS limq.sessions (
session_id String,
visitor_id String,
link_id String,
started_at DateTime64(3),
last_activity DateTime64(3),
ended_at Nullable(DateTime64(3)),
duration_sec UInt32 DEFAULT 0,
session_pages UInt8 DEFAULT 1,
is_completed Boolean DEFAULT false,
PRIMARY KEY (session_id)
) ENGINE = ReplacingMergeTree(last_activity)
ORDER BY
(session_id, link_id, visitor_id) SETTINGS index_granularity = 8192;
-- QR码统计表
CREATE TABLE IF NOT EXISTS limq.qr_scans (
scan_id UUID DEFAULT generateUUIDv4(),
qr_code_id String,
link_id String,
scan_time DateTime64(3),
visitor_id String,
location String,
device_type Enum8(
'mobile' = 1,
'tablet' = 2,
'desktop' = 3,
'other' = 4
),
led_to_conversion Boolean DEFAULT false,
PRIMARY KEY (scan_id)
) ENGINE = MergeTree() PARTITION BY toYYYYMM(scan_time)
ORDER BY
(qr_code_id, scan_time) SETTINGS index_granularity = 8192;
-- 每日链接汇总视图
CREATE MATERIALIZED VIEW limq.link_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
ORDER BY
(date, link_id) SETTINGS index_granularity = 8192 AS
SELECT
toDate(event_time) AS date,
link_id,
count() AS total_clicks,
uniqExact(visitor_id) AS unique_visitors,
uniqExact(session_id) AS unique_sessions,
sum(time_spent_sec) AS total_time_spent,
avg(time_spent_sec) AS avg_time_spent,
countIf(is_bounce) AS bounce_count,
countIf(event_type = 'conversion') AS conversion_count,
uniqExact(referrer) AS unique_referrers,
countIf(device_type = 'mobile') AS mobile_count,
countIf(device_type = 'tablet') AS tablet_count,
countIf(device_type = 'desktop') AS desktop_count,
countIf(is_qr_scan) AS qr_scan_count,
sum(conversion_value) AS total_conversion_value
FROM
limq.link_events
GROUP BY
date,
link_id;
-- 每小时访问模式视图
CREATE MATERIALIZED VIEW limq.link_hourly_patterns ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
ORDER BY
(date, hour, link_id) SETTINGS index_granularity = 8192 AS
SELECT
toDate(event_time) AS date,
toHour(event_time) AS hour,
link_id,
count() AS visits,
uniqExact(visitor_id) AS unique_visitors
FROM
limq.link_events
GROUP BY
date,
hour,
link_id;
-- 平台分布视图
CREATE MATERIALIZED VIEW limq.platform_distribution ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
ORDER BY
(date, utm_source, device_type) SETTINGS index_granularity = 8192 AS
SELECT
toDate(event_time) AS date,
utm_source,
device_type,
count() AS visits,
uniqExact(visitor_id) AS unique_visitors
FROM
limq.link_events
WHERE
utm_source != ''
GROUP BY
date,
utm_source,
device_type;

View File

@@ -0,0 +1,146 @@
-- 添加team、project和qrcode表到limq数据库
USE limq;
-- 团队表
CREATE TABLE IF NOT EXISTS limq.teams (
team_id String,
name String,
created_at DateTime,
created_by String,
description String DEFAULT '',
avatar_url String DEFAULT '',
is_active Boolean DEFAULT true,
plan_type Enum8(
'free' = 1,
'pro' = 2,
'enterprise' = 3
),
members_count UInt32 DEFAULT 1,
PRIMARY KEY (team_id)
) ENGINE = ReplacingMergeTree()
ORDER BY
team_id SETTINGS index_granularity = 8192;
-- 项目表
CREATE TABLE IF NOT EXISTS limq.projects (
project_id String,
team_id String,
name String,
created_at DateTime,
created_by String,
description String DEFAULT '',
is_archived Boolean DEFAULT false,
links_count UInt32 DEFAULT 0,
total_clicks UInt64 DEFAULT 0,
last_updated DateTime DEFAULT now(),
PRIMARY KEY (project_id)
) ENGINE = ReplacingMergeTree()
ORDER BY
(project_id, team_id) SETTINGS index_granularity = 8192;
-- QR码表 (扩展现有的qr_scans表)
CREATE TABLE IF NOT EXISTS limq.qrcodes (
qr_code_id String,
link_id String,
team_id String,
project_id String DEFAULT '',
name String,
description String DEFAULT '',
created_at DateTime,
created_by String,
updated_at DateTime DEFAULT now(),
qr_type Enum8(
'standard' = 1,
'custom' = 2,
'dynamic' = 3
) DEFAULT 'standard',
image_url String DEFAULT '',
design_config String DEFAULT '{}',
is_active Boolean DEFAULT true,
total_scans UInt64 DEFAULT 0,
unique_scanners UInt32 DEFAULT 0,
PRIMARY KEY (qr_code_id)
) ENGINE = ReplacingMergeTree()
ORDER BY
(qr_code_id, link_id) SETTINGS index_granularity = 8192;
-- 团队成员表
CREATE TABLE IF NOT EXISTS limq.team_members (
team_id String,
user_id String,
role Enum8(
'owner' = 1,
'admin' = 2,
'editor' = 3,
'viewer' = 4
),
joined_at DateTime DEFAULT now(),
invited_by String,
is_active Boolean DEFAULT true,
last_active DateTime DEFAULT now(),
PRIMARY KEY (team_id, user_id)
) ENGINE = ReplacingMergeTree()
ORDER BY
(team_id, user_id) SETTINGS index_granularity = 8192;
-- 团队每日统计视图
CREATE MATERIALIZED VIEW limq.team_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
ORDER BY
(date, team_id) SETTINGS index_granularity = 8192 AS
SELECT
toDate(event_time) AS date,
l.team_id AS team_id,
count() AS total_clicks,
uniqExact(e.visitor_id) AS unique_visitors,
countIf(e.event_type = 'conversion') AS conversion_count,
uniqExact(e.link_id) AS links_used,
countIf(e.is_qr_scan) AS qr_scan_count
FROM
limq.link_events e
JOIN limq.links l ON e.link_id = l.link_id
WHERE
l.team_id != ''
GROUP BY
date,
l.team_id;
-- 项目每日统计视图
CREATE MATERIALIZED VIEW limq.project_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
ORDER BY
(date, project_id) SETTINGS index_granularity = 8192 AS
SELECT
toDate(event_time) AS date,
l.project_id AS project_id,
count() AS total_clicks,
uniqExact(e.visitor_id) AS unique_visitors,
countIf(e.event_type = 'conversion') AS conversion_count,
uniqExact(e.link_id) AS links_used,
countIf(e.is_qr_scan) AS qr_scan_count
FROM
limq.link_events e
JOIN limq.links l ON e.link_id = l.link_id
WHERE
l.project_id != ''
GROUP BY
date,
l.project_id;
-- QR码每日统计视图
CREATE MATERIALIZED VIEW limq.qrcode_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
ORDER BY
(date, qr_code_id) SETTINGS index_granularity = 8192 AS
SELECT
toDate(scan_time) AS date,
qr_code_id,
count() AS total_scans,
uniqExact(visitor_id) AS unique_scanners,
countIf(led_to_conversion) AS conversions,
countIf(device_type = 'mobile') AS mobile_scans,
countIf(device_type = 'tablet') AS tablet_scans,
countIf(device_type = 'desktop') AS desktop_scans,
uniqExact(location) AS unique_locations
FROM
limq.qr_scans
GROUP BY
date,
qr_code_id;

View File

@@ -0,0 +1,997 @@
-- 移动端点击访问事件
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-15 10:25:30',
'2025-03-15',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-123',
's-456',
'click',
'103.45.67.89',
'China',
'Shanghai',
'https://www.google.com',
'google',
'organic',
'none',
'Mozilla/5.0 (iPhone)',
'mobile',
'Safari',
'iOS',
45,
false,
false,
'visit',
0
);
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-15 11:32:21',
'2025-03-15',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-124',
's-457',
'click',
'43.78.123.45',
'Japan',
'Tokyo',
'https://twitter.com',
'twitter',
'social',
'spring_promo',
'Mozilla/5.0 (Android 10)',
'mobile',
'Chrome',
'Android',
15,
true,
false,
'visit',
0
);
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-15 14:15:45',
'2025-03-15',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-125',
's-458',
'click',
'72.34.67.81',
'US',
'New York',
'https://www.facebook.com',
'facebook',
'social',
'crypto_ad',
'Mozilla/5.0 (iPhone)',
'mobile',
'Safari',
'iOS',
120,
false,
false,
'interact',
0
);
-- 桌面设备点击事件
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-15 08:45:12',
'2025-03-15',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-126',
's-459',
'click',
'89.67.43.21',
'Germany',
'Berlin',
'https://www.reddit.com',
'reddit',
'referral',
'none',
'Mozilla/5.0 (Windows NT 10.0)',
'desktop',
'Chrome',
'Windows',
300,
false,
false,
'visit',
0
);
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-15 16:20:33',
'2025-03-15',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-127',
's-460',
'click',
'178.65.43.12',
'UK',
'London',
'https://www.linkedin.com',
'linkedin',
'social',
'biz_campaign',
'Mozilla/5.0 (Macintosh)',
'desktop',
'Safari',
'MacOS',
250,
false,
false,
'stay',
0
);
-- 平板设备点击事件
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-15 13:10:55',
'2025-03-15',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-128',
's-461',
'click',
'156.78.34.12',
'Canada',
'Toronto',
'https://www.youtube.com',
'youtube',
'video',
'tutorial',
'Mozilla/5.0 (iPad)',
'tablet',
'Safari',
'iOS',
180,
false,
false,
'visit',
0
);
-- QR扫描访问事件
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-15 09:30:22',
'2025-03-15',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_qr',
'v-129',
's-462',
'click',
'101.56.78.90',
'China',
'Beijing',
'direct',
'qr',
'print',
'offline_event',
'Mozilla/5.0 (iPhone)',
'mobile',
'Safari',
'iOS',
75,
false,
true,
'visit',
0
);
-- 转化事件
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-15 10:27:45',
'2025-03-15',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-123',
's-456',
'conversion',
'103.45.67.89',
'China',
'Shanghai',
'https://www.google.com',
'google',
'organic',
'none',
'Mozilla/5.0 (iPhone)',
'mobile',
'Safari',
'iOS',
120,
false,
false,
'signup',
50
);
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-15 08:52:18',
'2025-03-15',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-126',
's-459',
'conversion',
'89.67.43.21',
'Germany',
'Berlin',
'https://www.reddit.com',
'reddit',
'referral',
'none',
'Mozilla/5.0 (Windows NT 10.0)',
'desktop',
'Chrome',
'Windows',
450,
false,
false,
'purchase',
150.75
);
-- 第二天的数据 (3/16)
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-16 11:15:30',
'2025-03-16',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-130',
's-463',
'click',
'178.91.45.67',
'France',
'Paris',
'https://www.google.com',
'google',
'organic',
'none',
'Mozilla/5.0 (Android 11)',
'mobile',
'Chrome',
'Android',
60,
false,
false,
'visit',
0
);
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-16 14:22:45',
'2025-03-16',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-131',
's-464',
'click',
'89.123.45.78',
'Spain',
'Madrid',
'https://www.instagram.com',
'instagram',
'social',
'influencer',
'Mozilla/5.0 (iPhone)',
'mobile',
'Safari',
'iOS',
90,
false,
false,
'interact',
0
);
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-16 16:40:12',
'2025-03-16',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-131',
's-464',
'conversion',
'89.123.45.78',
'Spain',
'Madrid',
'https://www.instagram.com',
'instagram',
'social',
'influencer',
'Mozilla/5.0 (iPhone)',
'mobile',
'Safari',
'iOS',
200,
false,
false,
'subscription',
75.50
);
-- 第三天数据 (3/17)
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-17 09:10:22',
'2025-03-17',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-132',
's-465',
'click',
'45.67.89.123',
'US',
'Los Angeles',
'https://www.google.com',
'google',
'cpc',
'spring_sale',
'Mozilla/5.0 (Windows NT 10.0)',
'desktop',
'Edge',
'Windows',
150,
false,
false,
'visit',
0
);
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-17 12:30:45',
'2025-03-17',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-133',
's-466',
'click',
'67.89.123.45',
'Brazil',
'Sao Paulo',
'https://www.yahoo.com',
'yahoo',
'organic',
'none',
'Mozilla/5.0 (iPad)',
'tablet',
'Safari',
'iOS',
120,
false,
false,
'stay',
0
);
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-17 15:45:33',
'2025-03-17',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-132',
's-465',
'conversion',
'45.67.89.123',
'US',
'Los Angeles',
'https://www.google.com',
'google',
'cpc',
'spring_sale',
'Mozilla/5.0 (Windows NT 10.0)',
'desktop',
'Edge',
'Windows',
300,
false,
false,
'purchase',
225.50
);
-- 添加一周前的数据 (对比期)
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-08 10:25:30',
'2025-03-08',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-140',
's-470',
'click',
'103.45.67.89',
'China',
'Shanghai',
'https://www.google.com',
'google',
'organic',
'none',
'Mozilla/5.0 (iPhone)',
'mobile',
'Safari',
'iOS',
30,
false,
false,
'visit',
0
);
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-08 11:32:21',
'2025-03-08',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-141',
's-471',
'click',
'89.67.43.21',
'Germany',
'Berlin',
'https://www.reddit.com',
'reddit',
'referral',
'none',
'Mozilla/5.0 (Windows NT 10.0)',
'desktop',
'Chrome',
'Windows',
200,
false,
false,
'visit',
0
);
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
conversion_type,
conversion_value
)
VALUES
(
generateUUIDv4(),
'2025-03-08 13:10:55',
'2025-03-08',
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
'ch_main',
'v-140',
's-470',
'conversion',
'103.45.67.89',
'China',
'Shanghai',
'https://www.google.com',
'google',
'organic',
'none',
'Mozilla/5.0 (iPhone)',
'mobile',
'Safari',
'iOS',
100,
false,
false,
'purchase',
100.00
);

View File

@@ -0,0 +1,193 @@
-- 删除现有的物化视图(需要先删除视图,因为它们依赖于表)
DROP TABLE IF EXISTS limq.platform_distribution;
DROP TABLE IF EXISTS limq.link_hourly_patterns;
DROP TABLE IF EXISTS limq.link_daily_stats;
-- 删除现有的表
DROP TABLE IF EXISTS limq.qr_scans;
DROP TABLE IF EXISTS limq.sessions;
DROP TABLE IF EXISTS limq.link_events;
DROP TABLE IF EXISTS limq.links;
-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS limq;
-- 切换到limq数据库
USE limq;
-- 创建短链接访问事件表
CREATE TABLE IF NOT EXISTS limq.link_events (
event_id UUID DEFAULT generateUUIDv4(),
event_time DateTime64(3) DEFAULT now64(),
date Date DEFAULT toDate(event_time),
link_id String,
channel_id String,
visitor_id String,
session_id String,
event_type Enum8(
'click' = 1,
'redirect' = 2,
'conversion' = 3,
'error' = 4
),
-- 访问者信息
ip_address String,
country String,
city String,
-- 来源信息
referrer String,
utm_source String,
utm_medium String,
utm_campaign String,
-- 设备信息
user_agent String,
device_type Enum8(
'mobile' = 1,
'tablet' = 2,
'desktop' = 3,
'other' = 4
),
browser String,
os String,
-- 交互信息
time_spent_sec UInt32 DEFAULT 0,
is_bounce Boolean DEFAULT true,
-- QR码相关
is_qr_scan Boolean DEFAULT false,
qr_code_id String DEFAULT '',
-- 转化数据
conversion_type Enum8(
'visit' = 1,
'stay' = 2,
'interact' = 3,
'signup' = 4,
'subscription' = 5,
'purchase' = 6
) DEFAULT 'visit',
conversion_value Float64 DEFAULT 0,
-- 其他属性
custom_data String DEFAULT '{}'
) ENGINE = MergeTree() PARTITION BY toYYYYMM(date)
ORDER BY
(date, link_id, event_time) SETTINGS index_granularity = 8192;
-- 短链接维度表
CREATE TABLE IF NOT EXISTS limq.links (
link_id String,
original_url String,
created_at DateTime64(3),
created_by String,
title String,
description String,
tags Array(String),
is_active Boolean DEFAULT true,
expires_at Nullable(DateTime64(3)),
team_id String DEFAULT '',
project_id String DEFAULT '',
PRIMARY KEY (link_id)
) ENGINE = ReplacingMergeTree()
ORDER BY
link_id SETTINGS index_granularity = 8192;
-- 会话跟踪表
CREATE TABLE IF NOT EXISTS limq.sessions (
session_id String,
visitor_id String,
link_id String,
started_at DateTime64(3),
last_activity DateTime64(3),
ended_at Nullable(DateTime64(3)),
duration_sec UInt32 DEFAULT 0,
session_pages UInt8 DEFAULT 1,
is_completed Boolean DEFAULT false,
PRIMARY KEY (session_id)
) ENGINE = ReplacingMergeTree(last_activity)
ORDER BY
(session_id, link_id, visitor_id) SETTINGS index_granularity = 8192;
-- QR码统计表
CREATE TABLE IF NOT EXISTS limq.qr_scans (
scan_id UUID DEFAULT generateUUIDv4(),
qr_code_id String,
link_id String,
scan_time DateTime64(3),
visitor_id String,
location String,
device_type Enum8(
'mobile' = 1,
'tablet' = 2,
'desktop' = 3,
'other' = 4
),
led_to_conversion Boolean DEFAULT false,
PRIMARY KEY (scan_id)
) ENGINE = MergeTree() PARTITION BY toYYYYMM(scan_time)
ORDER BY
scan_id SETTINGS index_granularity = 8192;
-- 每日链接汇总视图
CREATE MATERIALIZED VIEW IF NOT EXISTS limq.link_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
ORDER BY
(date, link_id) SETTINGS index_granularity = 8192 AS
SELECT
toDate(event_time) AS date,
link_id,
count() AS total_clicks,
uniqExact(visitor_id) AS unique_visitors,
uniqExact(session_id) AS unique_sessions,
sum(time_spent_sec) AS total_time_spent,
avg(time_spent_sec) AS avg_time_spent,
countIf(is_bounce) AS bounce_count,
countIf(event_type = 'conversion') AS conversion_count,
uniqExact(referrer) AS unique_referrers,
countIf(device_type = 'mobile') AS mobile_count,
countIf(device_type = 'tablet') AS tablet_count,
countIf(device_type = 'desktop') AS desktop_count,
countIf(is_qr_scan) AS qr_scan_count,
sum(conversion_value) AS total_conversion_value
FROM
limq.link_events
GROUP BY
date,
link_id;
-- 每小时访问模式视图
CREATE MATERIALIZED VIEW IF NOT EXISTS limq.link_hourly_patterns ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
ORDER BY
(date, hour, link_id) SETTINGS index_granularity = 8192 AS
SELECT
toDate(event_time) AS date,
toHour(event_time) AS hour,
link_id,
count() AS visits,
uniqExact(visitor_id) AS unique_visitors
FROM
limq.link_events
GROUP BY
date,
hour,
link_id;
-- 平台分布视图
CREATE MATERIALIZED VIEW IF NOT EXISTS limq.platform_distribution ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
ORDER BY
(date, utm_source, device_type) SETTINGS index_granularity = 8192 AS
SELECT
toDate(event_time) AS date,
utm_source,
device_type,
count() AS visits,
uniqExact(visitor_id) AS unique_visitors
FROM
limq.link_events
WHERE
utm_source != ''
GROUP BY
date,
utm_source,
device_type;

View File

@@ -0,0 +1,828 @@
-- 清空现有数据(可选)
TRUNCATE TABLE IF EXISTS limq.link_events;
TRUNCATE TABLE IF EXISTS limq.link_daily_stats;
TRUNCATE TABLE IF EXISTS limq.link_hourly_patterns;
TRUNCATE TABLE IF EXISTS limq.links;
-- 使用固定的UUID值插入链接
INSERT INTO
limq.links (
link_id,
original_url,
created_at,
created_by,
title,
description,
tags,
is_active
)
VALUES
(
'11111111-1111-1111-1111-111111111111',
'https://example.com/page1',
now(),
'user-1',
'产品页面',
'我们的主要产品页面',
[ '产品',
'营销' ],
true
);
INSERT INTO
limq.links (
link_id,
original_url,
created_at,
created_by,
title,
description,
tags,
is_active
)
VALUES
(
'22222222-2222-2222-2222-222222222222',
'https://example.com/promo',
now(),
'user-1',
'促销活动',
'夏季特别促销活动',
[ '促销',
'活动' ],
true
);
INSERT INTO
limq.links (
link_id,
original_url,
created_at,
created_by,
title,
description,
tags,
is_active
)
VALUES
(
'33333333-3333-3333-3333-333333333333',
'https://example.com/blog',
now(),
'user-2',
'公司博客',
'公司新闻和更新',
[ '博客',
'内容' ],
true
);
INSERT INTO
limq.links (
link_id,
original_url,
created_at,
created_by,
title,
description,
tags,
is_active
)
VALUES
(
'44444444-4444-4444-4444-444444444444',
'https://example.com/signup',
now(),
'user-2',
'注册页面',
'新用户注册页面',
[ '转化',
'注册' ],
true
);
-- 为第一个链接创建500条记录
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
qr_code_id,
conversion_type,
conversion_value,
custom_data
)
SELECT
generateUUIDv4() AS event_id,
subtractDays(now(), rand() % 30) AS event_time,
toDate(event_time) AS date,
'11111111-1111-1111-1111-111111111111' AS link_id,
'channel-1' AS channel_id,
concat('visitor-', toString(rand() % 100 + 1)) AS visitor_id,
concat('session-', toString(number % 50 + 1)) AS session_id,
multiIf(
rand() % 100 < 70,
'click',
rand() % 100 < 90,
'redirect',
rand() % 100 < 98,
'conversion',
'error'
) AS event_type,
concat('192.168.1.', toString(rand() % 255)) AS ip_address,
multiIf(
rand() % 100 < 60,
'China',
rand() % 100 < 85,
'US',
rand() % 100 < 95,
'Japan',
'Other'
) AS country,
multiIf(
rand() % 100 < 60,
'Beijing',
rand() % 100 < 85,
'New York',
rand() % 100 < 95,
'Tokyo',
'Other'
) AS city,
multiIf(
rand() % 100 < 30,
'https://google.com',
rand() % 100 < 50,
'https://facebook.com',
rand() % 100 < 65,
'https://twitter.com',
rand() % 100 < 75,
'https://instagram.com',
rand() % 100 < 85,
'https://linkedin.com',
rand() % 100 < 90,
'https://bing.com',
rand() % 100 < 95,
'https://baidu.com',
'direct'
) AS referrer,
multiIf(
rand() % 100 < 40,
'google',
rand() % 100 < 70,
'facebook',
rand() % 100 < 90,
'email',
'direct'
) AS utm_source,
multiIf(
rand() % 100 < 40,
'cpc',
rand() % 100 < 70,
'social',
rand() % 100 < 90,
'email',
'direct'
) AS utm_medium,
multiIf(
rand() % 100 < 40,
'summer_sale',
rand() % 100 < 70,
'product_launch',
rand() % 100 < 90,
'newsletter',
'brand'
) AS utm_campaign,
'Mozilla/5.0' AS user_agent,
multiIf(
rand() % 100 < 60,
'mobile',
rand() % 100 < 85,
'desktop',
rand() % 100 < 95,
'tablet',
'other'
) AS device_type,
multiIf(
rand() % 100 < 50,
'Chrome',
rand() % 100 < 80,
'Safari',
rand() % 100 < 95,
'Firefox',
'Edge'
) AS browser,
multiIf(
rand() % 100 < 50,
'iOS',
rand() % 100 < 90,
'Android',
'Windows'
) AS os,
rand() % 300 AS time_spent_sec,
rand() % 100 < 25 AS is_bounce,
rand() % 100 < 20 AS is_qr_scan,
concat('qr-', toString(rand() % 10 + 1)) AS qr_code_id,
multiIf(
rand() % 100 < 50,
'visit',
rand() % 100 < 70,
'stay',
rand() % 100 < 85,
'interact',
rand() % 100 < 93,
'signup',
rand() % 100 < 97,
'subscription',
'purchase'
) AS conversion_type,
rand() % 100 * 1.5 AS conversion_value,
'{}' AS custom_data
FROM
numbers(500);
-- 为第二个链接创建300条记录
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
qr_code_id,
conversion_type,
conversion_value,
custom_data
)
SELECT
generateUUIDv4() AS event_id,
subtractDays(now(), rand() % 30) AS event_time,
toDate(event_time) AS date,
'22222222-2222-2222-2222-222222222222' AS link_id,
'channel-1' AS channel_id,
concat('visitor-', toString(rand() % 100 + 1)) AS visitor_id,
concat('session-', toString(number % 40 + 1)) AS session_id,
multiIf(
rand() % 100 < 70,
'click',
rand() % 100 < 90,
'redirect',
rand() % 100 < 98,
'conversion',
'error'
) AS event_type,
concat('192.168.1.', toString(rand() % 255)) AS ip_address,
multiIf(
rand() % 100 < 60,
'China',
rand() % 100 < 85,
'US',
rand() % 100 < 95,
'Japan',
'Other'
) AS country,
multiIf(
rand() % 100 < 60,
'Beijing',
rand() % 100 < 85,
'New York',
rand() % 100 < 95,
'Tokyo',
'Other'
) AS city,
multiIf(
rand() % 100 < 30,
'https://google.com',
rand() % 100 < 50,
'https://facebook.com',
rand() % 100 < 65,
'https://twitter.com',
rand() % 100 < 75,
'https://instagram.com',
rand() % 100 < 85,
'https://linkedin.com',
rand() % 100 < 90,
'https://bing.com',
rand() % 100 < 95,
'https://baidu.com',
'direct'
) AS referrer,
multiIf(
rand() % 100 < 40,
'google',
rand() % 100 < 70,
'facebook',
rand() % 100 < 90,
'email',
'direct'
) AS utm_source,
multiIf(
rand() % 100 < 40,
'cpc',
rand() % 100 < 70,
'social',
rand() % 100 < 90,
'email',
'direct'
) AS utm_medium,
multiIf(
rand() % 100 < 40,
'summer_sale',
rand() % 100 < 70,
'product_launch',
rand() % 100 < 90,
'newsletter',
'brand'
) AS utm_campaign,
'Mozilla/5.0' AS user_agent,
multiIf(
rand() % 100 < 60,
'mobile',
rand() % 100 < 85,
'desktop',
rand() % 100 < 95,
'tablet',
'other'
) AS device_type,
multiIf(
rand() % 100 < 50,
'Chrome',
rand() % 100 < 80,
'Safari',
rand() % 100 < 95,
'Firefox',
'Edge'
) AS browser,
multiIf(
rand() % 100 < 50,
'iOS',
rand() % 100 < 90,
'Android',
'Windows'
) AS os,
rand() % 300 AS time_spent_sec,
rand() % 100 < 25 AS is_bounce,
rand() % 100 < 15 AS is_qr_scan,
concat('qr-', toString(rand() % 10 + 1)) AS qr_code_id,
multiIf(
rand() % 100 < 50,
'visit',
rand() % 100 < 70,
'stay',
rand() % 100 < 85,
'interact',
rand() % 100 < 93,
'signup',
rand() % 100 < 97,
'subscription',
'purchase'
) AS conversion_type,
rand() % 100 * 2.5 AS conversion_value,
'{}' AS custom_data
FROM
numbers(300);
-- 为第三个链接创建200条记录
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
qr_code_id,
conversion_type,
conversion_value,
custom_data
)
SELECT
generateUUIDv4() AS event_id,
subtractDays(now(), rand() % 30) AS event_time,
toDate(event_time) AS date,
'33333333-3333-3333-3333-333333333333' AS link_id,
'channel-2' AS channel_id,
concat('visitor-', toString(rand() % 100 + 1)) AS visitor_id,
concat('session-', toString(number % 30 + 1)) AS session_id,
multiIf(
rand() % 100 < 70,
'click',
rand() % 100 < 90,
'redirect',
rand() % 100 < 98,
'conversion',
'error'
) AS event_type,
concat('192.168.1.', toString(rand() % 255)) AS ip_address,
multiIf(
rand() % 100 < 60,
'China',
rand() % 100 < 85,
'US',
rand() % 100 < 95,
'Japan',
'Other'
) AS country,
multiIf(
rand() % 100 < 60,
'Beijing',
rand() % 100 < 85,
'New York',
rand() % 100 < 95,
'Tokyo',
'Other'
) AS city,
multiIf(
rand() % 100 < 30,
'https://google.com',
rand() % 100 < 50,
'https://facebook.com',
rand() % 100 < 65,
'https://twitter.com',
rand() % 100 < 75,
'https://instagram.com',
rand() % 100 < 85,
'https://linkedin.com',
rand() % 100 < 90,
'https://bing.com',
rand() % 100 < 95,
'https://baidu.com',
'direct'
) AS referrer,
multiIf(
rand() % 100 < 40,
'google',
rand() % 100 < 70,
'facebook',
rand() % 100 < 90,
'email',
'direct'
) AS utm_source,
multiIf(
rand() % 100 < 40,
'cpc',
rand() % 100 < 70,
'social',
rand() % 100 < 90,
'email',
'direct'
) AS utm_medium,
multiIf(
rand() % 100 < 40,
'summer_sale',
rand() % 100 < 70,
'product_launch',
rand() % 100 < 90,
'newsletter',
'brand'
) AS utm_campaign,
'Mozilla/5.0' AS user_agent,
multiIf(
rand() % 100 < 60,
'mobile',
rand() % 100 < 85,
'desktop',
rand() % 100 < 95,
'tablet',
'other'
) AS device_type,
multiIf(
rand() % 100 < 50,
'Chrome',
rand() % 100 < 80,
'Safari',
rand() % 100 < 95,
'Firefox',
'Edge'
) AS browser,
multiIf(
rand() % 100 < 50,
'iOS',
rand() % 100 < 90,
'Android',
'Windows'
) AS os,
rand() % 600 AS time_spent_sec,
rand() % 100 < 15 AS is_bounce,
rand() % 100 < 10 AS is_qr_scan,
concat('qr-', toString(rand() % 10 + 1)) AS qr_code_id,
multiIf(
rand() % 100 < 50,
'visit',
rand() % 100 < 70,
'stay',
rand() % 100 < 85,
'interact',
rand() % 100 < 93,
'signup',
rand() % 100 < 97,
'subscription',
'purchase'
) AS conversion_type,
rand() % 100 * 1.2 AS conversion_value,
'{}' AS custom_data
FROM
numbers(200);
-- 为第四个链接创建400条记录
INSERT INTO
limq.link_events (
event_id,
event_time,
date,
link_id,
channel_id,
visitor_id,
session_id,
event_type,
ip_address,
country,
city,
referrer,
utm_source,
utm_medium,
utm_campaign,
user_agent,
device_type,
browser,
os,
time_spent_sec,
is_bounce,
is_qr_scan,
qr_code_id,
conversion_type,
conversion_value,
custom_data
)
SELECT
generateUUIDv4() AS event_id,
subtractDays(now(), rand() % 30) AS event_time,
toDate(event_time) AS date,
'44444444-4444-4444-4444-444444444444' AS link_id,
'channel-2' AS channel_id,
concat('visitor-', toString(rand() % 100 + 1)) AS visitor_id,
concat('session-', toString(number % 60 + 1)) AS session_id,
multiIf(
rand() % 100 < 70,
'click',
rand() % 100 < 90,
'redirect',
rand() % 100 < 98,
'conversion',
'error'
) AS event_type,
concat('192.168.1.', toString(rand() % 255)) AS ip_address,
multiIf(
rand() % 100 < 60,
'China',
rand() % 100 < 85,
'US',
rand() % 100 < 95,
'Japan',
'Other'
) AS country,
multiIf(
rand() % 100 < 60,
'Beijing',
rand() % 100 < 85,
'New York',
rand() % 100 < 95,
'Tokyo',
'Other'
) AS city,
multiIf(
rand() % 100 < 30,
'https://google.com',
rand() % 100 < 50,
'https://facebook.com',
rand() % 100 < 65,
'https://twitter.com',
rand() % 100 < 75,
'https://instagram.com',
rand() % 100 < 85,
'https://linkedin.com',
rand() % 100 < 90,
'https://bing.com',
rand() % 100 < 95,
'https://baidu.com',
'direct'
) AS referrer,
multiIf(
rand() % 100 < 40,
'google',
rand() % 100 < 70,
'facebook',
rand() % 100 < 90,
'email',
'direct'
) AS utm_source,
multiIf(
rand() % 100 < 40,
'cpc',
rand() % 100 < 70,
'social',
rand() % 100 < 90,
'email',
'direct'
) AS utm_medium,
multiIf(
rand() % 100 < 40,
'summer_sale',
rand() % 100 < 70,
'product_launch',
rand() % 100 < 90,
'newsletter',
'brand'
) AS utm_campaign,
'Mozilla/5.0' AS user_agent,
multiIf(
rand() % 100 < 60,
'mobile',
rand() % 100 < 85,
'desktop',
rand() % 100 < 95,
'tablet',
'other'
) AS device_type,
multiIf(
rand() % 100 < 50,
'Chrome',
rand() % 100 < 80,
'Safari',
rand() % 100 < 95,
'Firefox',
'Edge'
) AS browser,
multiIf(
rand() % 100 < 50,
'iOS',
rand() % 100 < 90,
'Android',
'Windows'
) AS os,
rand() % 400 AS time_spent_sec,
rand() % 100 < 20 AS is_bounce,
rand() % 100 < 25 AS is_qr_scan,
concat('qr-', toString(rand() % 10 + 1)) AS qr_code_id,
multiIf(
rand() % 100 < 50,
'visit',
rand() % 100 < 70,
'stay',
rand() % 100 < 85,
'interact',
rand() % 100 < 93,
'signup',
rand() % 100 < 97,
'subscription',
'purchase'
) AS conversion_type,
rand() % 100 * 3.5 AS conversion_value,
'{}' AS custom_data
FROM
numbers(400);
-- 插入link_daily_stats表数据
INSERT INTO
limq.link_daily_stats (
date,
link_id,
total_clicks,
unique_visitors,
unique_sessions,
total_time_spent,
avg_time_spent,
bounce_count,
conversion_count,
unique_referrers,
mobile_count,
tablet_count,
desktop_count,
qr_scan_count,
total_conversion_value
)
SELECT
subtractDays(today(), number) AS date,
multiIf(
number % 4 = 0,
'11111111-1111-1111-1111-111111111111',
number % 4 = 1,
'22222222-2222-2222-2222-222222222222',
number % 4 = 2,
'33333333-3333-3333-3333-333333333333',
'44444444-4444-4444-4444-444444444444'
) AS link_id,
50 + rand() % 100 AS total_clicks,
30 + rand() % 50 AS unique_visitors,
20 + rand() % 40 AS unique_sessions,
(500 + rand() % 1000) * 60 AS total_time_spent,
(rand() % 10) * 60 + rand() % 60 AS avg_time_spent,
5 + rand() % 20 AS bounce_count,
rand() % 30 AS conversion_count,
3 + rand() % 8 AS unique_referrers,
20 + rand() % 40 AS mobile_count,
5 + rand() % 15 AS tablet_count,
15 + rand() % 30 AS desktop_count,
rand() % 10 AS qr_scan_count,
rand() % 1000 * 2.5 AS total_conversion_value
FROM
numbers(30)
WHERE
number < 30;
-- 插入link_hourly_patterns表数据
INSERT INTO
limq.link_hourly_patterns (date, hour, link_id, visits, unique_visitors)
SELECT
subtractDays(today(), number % 7) AS date,
number % 24 AS hour,
multiIf(
intDiv(number, 24) % 4 = 0,
'11111111-1111-1111-1111-111111111111',
intDiv(number, 24) % 4 = 1,
'22222222-2222-2222-2222-222222222222',
intDiv(number, 24) % 4 = 2,
'33333333-3333-3333-3333-333333333333',
'44444444-4444-4444-4444-444444444444'
) AS link_id,
5 + rand() % 20 AS visits,
3 + rand() % 10 AS unique_visitors
FROM
numbers(672) -- 7天 x 24小时 x 4个链接
WHERE
number < 672;
-- 显示数据行数,验证插入成功
SELECT
'link_events 表行数:' AS metric,
count() AS value
FROM
limq.link_events
UNION
ALL
SELECT
'link_daily_stats 表行数:',
count()
FROM
limq.link_daily_stats
UNION
ALL
SELECT
'link_hourly_patterns 表行数:',
count()
FROM
limq.link_hourly_patterns;

View File

@@ -0,0 +1,331 @@
#!/usr/bin/env node
// # 显示所有表
// ./pg-query.js -t
// # 显示表结构
// ./pg-query.js -d influencers
// # 显示样本数据限制5行
// ./pg-query.js -s posts -l 5
// # 查看表记录数
// ./pg-query.js -c posts
// # 显示索引
// ./pg-query.js -i posts
// # 显示外键
// ./pg-query.js -f posts
// # 显示引用
// ./pg-query.js -r influencers
// # 执行自定义查询
// ./pg-query.js -q "SELECT * FROM influencers WHERE platform = 'Instagram' LIMIT 5"
// # 执行SQL文件
// ./pg-query.js -e schema.sql
const { Client } = require('pg');
const path = require('path');
const fs = require('fs');
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
// 加载.env文件 - 使用正确的相对路径
require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') });
// 显示连接信息(不含密码)以便调试
function getConnectionString() {
// 使用.env中的DATABASE_URL
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error('错误: 未找到DATABASE_URL环境变量');
process.exit(1);
}
// 显示连接信息但隐藏密码
const sanitizedUrl = databaseUrl.replace(/:[^:@]+@/, ':***@');
console.log(`使用连接: ${sanitizedUrl}`);
return databaseUrl;
}
// 创建一个新的客户端
async function runQuery(query, params = []) {
const client = new Client({
connectionString: getConnectionString()
});
try {
await client.connect();
console.log('数据库连接成功');
const result = await client.query(query, params);
return result.rows;
} catch (err) {
console.error('查询执行错误:', err.message);
return null;
} finally {
await client.end();
}
}
// 显示所有表
async function showTables() {
const query = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;";
const tables = await runQuery(query);
if (tables && tables.length > 0) {
console.log('数据库中的表:');
console.table(tables);
} else {
console.log('没有找到表或连接失败');
}
}
// 显示表结构
async function showTableStructure(tableName) {
const query = `
SELECT
column_name AS "列名",
data_type AS "数据类型",
CASE WHEN is_nullable = 'YES' THEN '允许为空' ELSE '不允许为空' END AS "是否可空",
column_default AS "默认值",
character_maximum_length AS "最大长度"
FROM
information_schema.columns
WHERE
table_schema = 'public' AND
table_name = $1
ORDER BY
ordinal_position;
`;
const columns = await runQuery(query, [tableName]);
if (columns && columns.length > 0) {
console.log(`${tableName} 的结构:`);
console.table(columns);
} else {
console.log(`${tableName} 不存在或连接失败`);
}
}
// 显示样本数据
async function showSampleData(tableName, limit = 10) {
const query = `SELECT * FROM "${tableName}" LIMIT ${limit};`;
const data = await runQuery(query);
if (data && data.length > 0) {
console.log(`${tableName} 的样本数据 (${limit} 行):`);
console.table(data);
} else {
console.log(`${tableName} 为空或不存在`);
}
}
// 显示记录计数
async function showRecordCount(tableName) {
const query = `SELECT COUNT(*) AS "记录数" FROM "${tableName}";`;
const count = await runQuery(query);
if (count) {
console.log(`${tableName} 的记录数:`);
console.table(count);
} else {
console.log(`${tableName} 不存在或连接失败`);
}
}
// 显示索引信息
async function showIndexes(tableName) {
const query = `
SELECT
indexname AS "索引名称",
indexdef AS "索引定义"
FROM
pg_indexes
WHERE
tablename = $1
ORDER BY
indexname;
`;
const indexes = await runQuery(query, [tableName]);
if (indexes && indexes.length > 0) {
console.log(`${tableName} 的索引:`);
console.table(indexes);
} else {
console.log(`${tableName} 没有索引或不存在`);
}
}
// 显示外键
async function showForeignKeys(tableName) {
const query = `
SELECT
conname AS "外键名称",
pg_get_constraintdef(oid) AS "外键定义"
FROM
pg_constraint
WHERE
conrelid = $1::regclass AND contype = 'f';
`;
const foreignKeys = await runQuery(query, [tableName]);
if (foreignKeys && foreignKeys.length > 0) {
console.log(`${tableName} 的外键:`);
console.table(foreignKeys);
} else {
console.log(`${tableName} 没有外键或不存在`);
}
}
// 显示引用当前表的外键
async function showReferencingKeys(tableName) {
const query = `
SELECT
c.conname AS "外键名称",
t.relname AS "引用表",
pg_get_constraintdef(c.oid) AS "外键定义"
FROM
pg_constraint c
JOIN
pg_class t ON c.conrelid = t.oid
WHERE
c.confrelid = $1::regclass AND c.contype = 'f';
`;
const referencingKeys = await runQuery(query, [tableName]);
if (referencingKeys && referencingKeys.length > 0) {
console.log(`引用表 ${tableName} 的外键关系:`);
console.table(referencingKeys);
} else {
console.log(`没有找到引用表 ${tableName} 的外键关系`);
}
}
// 执行自定义查询
async function executeQuery(query) {
const result = await runQuery(query);
if (result) {
console.log('查询结果:');
console.table(result);
} else {
console.log('查询执行失败或无结果');
}
}
// 执行SQL文件
async function executeSqlFile(filename) {
try {
const sql = fs.readFileSync(filename, 'utf8');
console.log(`执行SQL文件: ${filename}`);
await executeQuery(sql);
} catch (err) {
console.error(`执行SQL文件失败: ${err.message}`);
}
}
// 主函数
async function main() {
try {
const argv = yargs(hideBin(process.argv))
.usage('PostgreSQL 查询工具\n\n用法: $0 [选项]')
.option('t', {
alias: 'tables',
describe: '显示所有表',
type: 'boolean'
})
.option('d', {
alias: 'describe',
describe: '显示表结构',
type: 'string'
})
.option('s', {
alias: 'sample',
describe: '显示表样本数据',
type: 'string'
})
.option('l', {
alias: 'limit',
describe: '样本数据行数限制',
type: 'number',
default: 10
})
.option('c', {
alias: 'count',
describe: '计算表中的记录数',
type: 'string'
})
.option('i', {
alias: 'indexes',
describe: '显示表索引',
type: 'string'
})
.option('f', {
alias: 'foreign-keys',
describe: '显示表外键关系',
type: 'string'
})
.option('r', {
alias: 'references',
describe: '显示引用此表的外键',
type: 'string'
})
.option('q', {
alias: 'query',
describe: '执行自定义SQL查询',
type: 'string'
})
.option('e', {
alias: 'execute-file',
describe: '执行SQL文件',
type: 'string'
})
.example('$0 -t', '显示所有表')
.example('$0 -d influencers', '显示influencers表结构')
.example('$0 -s posts -l 5', '显示posts表前5行数据')
.epilog('更多信息请访问项目文档')
.help()
.alias('h', 'help')
.argv;
if (argv.tables) {
await showTables();
} else if (argv.describe) {
await showTableStructure(argv.describe);
} else if (argv.sample) {
await showSampleData(argv.sample, argv.limit);
} else if (argv.count) {
await showRecordCount(argv.count);
} else if (argv.indexes) {
await showIndexes(argv.indexes);
} else if (argv.foreignKeys) {
await showForeignKeys(argv.foreignKeys);
} else if (argv.references) {
await showReferencingKeys(argv.references);
} else if (argv.query) {
await executeQuery(argv.query);
} else if (argv.executeFile) {
await executeSqlFile(argv.executeFile);
} else {
yargs(hideBin(process.argv)).showHelp();
}
} catch (err) {
console.error('程序执行错误:', err.message);
process.exit(1);
}
}
// 执行主函数
main().catch(err => {
console.error('程序执行错误:', err);
process.exit(1);
});

81
scripts/deploy.sh Normal file
View File

@@ -0,0 +1,81 @@
#!/bin/bash
set -e
set -x
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${YELLOW}开始部署流程...${NC}"
# 首先加载环境变量
if [ "$NODE_ENV" = "production" ]; then
echo -e "${GREEN}加载生产环境配置...${NC}"
set -a
source .env.production
set +a
else
echo -e "${GREEN}加载开发环境配置...${NC}"
set -a
source .env.development
set +a
fi
# 安装依赖
echo -e "${GREEN}安装依赖...${NC}"
NODE_ENV= pnpm install --ignore-workspace
# 生成 Prisma 客户端
echo -e "${GREEN}生成 Prisma 客户端...${NC}"
npx prisma generate
# 类型检查
echo -e "${GREEN}运行类型检查...${NC}"
pnpm tsc --noEmit
# 询问是否同步数据库架构
echo -e "${YELLOW}是否需要同步数据库架构? (y/n)${NC}"
read -r sync_db
if [ "$sync_db" = "y" ] || [ "$sync_db" = "Y" ]; then
echo -e "${GREEN}开始同步数据库架构...${NC}"
if [ "$NODE_ENV" = "production" ]; then
npx prisma db push
else
npx prisma db push
fi
else
echo -e "${YELLOW}跳过数据库同步${NC}"
fi
# 构建项目
echo -e "${GREEN}构建项目...${NC}"
pnpm build
# 检查并安装 PM2
echo -e "${GREEN}检查 PM2...${NC}"
if ! command -v pm2 &> /dev/null; then
echo -e "${YELLOW}PM2 未安装,正在安装 5.4.3 版本...${NC}"
pnpm add pm2@5.4.3 -g
else
PM2_VERSION=$(pm2 -v)
if [ "$PM2_VERSION" != "5.4.3" ]; then
echo -e "${YELLOW}错误: PM2 版本必须是 5.4.3,当前版本是 ${PM2_VERSION}${NC}"
echo -e "${YELLOW}请运行以下命令更新 PM2:${NC}"
echo -e "${YELLOW}pm2 kill && pnpm remove pm2 -g && rm -rf ~/.pm2 && pnpm add pm2@5.4.3 -g${NC}"
exit 1
else
echo -e "${GREEN}PM2 5.4.3 已安装${NC}"
fi
fi
# 启动服务
if [ "$NODE_ENV" = "production" ]; then
echo -e "${GREEN}以生产模式启动服务...${NC}"
pm2 start dist/src/main.js --name limq
else
echo -e "${GREEN}以开发模式启动服务...${NC}"
pm2 start dist/src/main.js --name limq-dev --watch
fi
echo -e "${GREEN}部署完成!${NC}"