init
This commit is contained in:
86
backend/src/utils/clickhouse.ts
Normal file
86
backend/src/utils/clickhouse.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { createClient } from '@clickhouse/client';
|
||||
import config from '../config';
|
||||
|
||||
// Create ClickHouse client with error handling
|
||||
const createClickHouseClient = () => {
|
||||
try {
|
||||
return createClient({
|
||||
host: `http://${config.clickhouse.host}:${config.clickhouse.port}`,
|
||||
username: config.clickhouse.user,
|
||||
password: config.clickhouse.password,
|
||||
database: config.clickhouse.database,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating ClickHouse client:', error);
|
||||
// Return a mock client for development that logs operations instead of executing them
|
||||
return {
|
||||
query: async ({ query, values }: { query: string; values?: any[] }) => {
|
||||
console.log('ClickHouse query (mock):', query, values);
|
||||
return { rows: [] };
|
||||
},
|
||||
close: async () => {
|
||||
console.log('ClickHouse connection closed (mock)');
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const clickhouse = createClickHouseClient();
|
||||
|
||||
// Initialize ClickHouse database and tables
|
||||
export const initClickHouse = async () => {
|
||||
try {
|
||||
// Create database if not exists
|
||||
await clickhouse.query({
|
||||
query: `CREATE DATABASE IF NOT EXISTS ${config.clickhouse.database}`,
|
||||
});
|
||||
|
||||
// Create tables for tracking events
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
CREATE TABLE IF NOT EXISTS ${config.clickhouse.database}.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)
|
||||
`,
|
||||
});
|
||||
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
CREATE TABLE IF NOT EXISTS ${config.clickhouse.database}.like_events (
|
||||
user_id String,
|
||||
content_id String,
|
||||
timestamp DateTime DEFAULT now(),
|
||||
action Enum('like' = 1, 'unlike' = 2)
|
||||
) ENGINE = MergeTree()
|
||||
PARTITION BY toYYYYMM(timestamp)
|
||||
ORDER BY (user_id, content_id, timestamp)
|
||||
`,
|
||||
});
|
||||
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
CREATE TABLE IF NOT EXISTS ${config.clickhouse.database}.follower_events (
|
||||
follower_id String,
|
||||
followed_id String,
|
||||
timestamp DateTime DEFAULT now(),
|
||||
action Enum('follow' = 1, 'unfollow' = 2)
|
||||
) ENGINE = MergeTree()
|
||||
PARTITION BY toYYYYMM(timestamp)
|
||||
ORDER BY (follower_id, followed_id, timestamp)
|
||||
`,
|
||||
});
|
||||
|
||||
console.log('ClickHouse database and tables initialized');
|
||||
} catch (error) {
|
||||
console.error('Error initializing ClickHouse:', error);
|
||||
console.log('Continuing with limited functionality...');
|
||||
}
|
||||
};
|
||||
|
||||
export default clickhouse;
|
||||
538
backend/src/utils/initDatabase.ts
Normal file
538
backend/src/utils/initDatabase.ts
Normal file
@@ -0,0 +1,538 @@
|
||||
import supabase from './supabase';
|
||||
import clickhouse from './clickhouse';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* 初始化 Supabase (PostgreSQL) 数据库表
|
||||
*/
|
||||
export const initSupabaseTables = async () => {
|
||||
try {
|
||||
console.log('开始初始化 Supabase 数据表...');
|
||||
|
||||
// 创建用户扩展表
|
||||
await supabase.rpc('create_user_profiles_if_not_exists');
|
||||
|
||||
// 创建项目表
|
||||
await supabase.rpc('create_projects_table_if_not_exists');
|
||||
|
||||
// 创建网红(影响者)表
|
||||
await supabase.rpc('create_influencers_table_if_not_exists');
|
||||
|
||||
// 创建项目-网红关联表
|
||||
await supabase.rpc('create_project_influencers_table_if_not_exists');
|
||||
|
||||
// 创建帖子表
|
||||
await supabase.rpc('create_posts_table_if_not_exists');
|
||||
|
||||
// 创建评论表
|
||||
await supabase.rpc('create_comments_table_if_not_exists');
|
||||
|
||||
// 创建项目评论表
|
||||
await supabase.rpc('create_project_comments_table_if_not_exists');
|
||||
|
||||
console.log('Supabase 数据表初始化完成');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('初始化 Supabase 数据表失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化 ClickHouse 数据库表
|
||||
*/
|
||||
export const initClickHouseTables = async () => {
|
||||
try {
|
||||
console.log('开始初始化 ClickHouse 数据表...');
|
||||
|
||||
// 创建事件表
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
event_id UUID DEFAULT generateUUIDv4(),
|
||||
project_id UUID,
|
||||
influencer_id UUID,
|
||||
post_id UUID NULL,
|
||||
platform String,
|
||||
event_type Enum(
|
||||
'follower_change' = 1,
|
||||
'post_like_change' = 2,
|
||||
'post_view_change' = 3,
|
||||
'click' = 4,
|
||||
'comment' = 5,
|
||||
'share' = 6,
|
||||
'project_comment' = 7
|
||||
),
|
||||
metric_value Int64,
|
||||
event_metadata String,
|
||||
timestamp DateTime DEFAULT now()
|
||||
) ENGINE = MergeTree()
|
||||
PARTITION BY toYYYYMM(timestamp)
|
||||
ORDER BY (platform, influencer_id, post_id, event_type, timestamp)
|
||||
`
|
||||
});
|
||||
|
||||
// 创建统计视图 - 按天统计
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS daily_stats
|
||||
ENGINE = SummingMergeTree()
|
||||
PARTITION BY toYYYYMM(date)
|
||||
ORDER BY (date, platform, influencer_id, event_type)
|
||||
AS SELECT
|
||||
toDate(timestamp) AS date,
|
||||
platform,
|
||||
influencer_id,
|
||||
event_type,
|
||||
SUM(metric_value) AS total_value,
|
||||
COUNT(*) AS event_count
|
||||
FROM events
|
||||
GROUP BY date, platform, influencer_id, event_type
|
||||
`
|
||||
});
|
||||
|
||||
// 创建统计视图 - 按月统计
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS monthly_stats
|
||||
ENGINE = SummingMergeTree()
|
||||
ORDER BY (month, platform, influencer_id, event_type)
|
||||
AS SELECT
|
||||
toStartOfMonth(timestamp) AS month,
|
||||
platform,
|
||||
influencer_id,
|
||||
event_type,
|
||||
SUM(metric_value) AS total_value,
|
||||
COUNT(*) AS event_count
|
||||
FROM events
|
||||
GROUP BY month, platform, influencer_id, event_type
|
||||
`
|
||||
});
|
||||
|
||||
// 创建帖子互动统计视图
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS post_interaction_stats
|
||||
ENGINE = SummingMergeTree()
|
||||
ORDER BY (post_id, event_type, date)
|
||||
AS SELECT
|
||||
post_id,
|
||||
event_type,
|
||||
toDate(timestamp) AS date,
|
||||
SUM(metric_value) AS value,
|
||||
COUNT(*) AS count
|
||||
FROM events
|
||||
WHERE post_id IS NOT NULL
|
||||
GROUP BY post_id, event_type, date
|
||||
`
|
||||
});
|
||||
|
||||
// 创建项目互动统计视图
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS project_interaction_stats
|
||||
ENGINE = SummingMergeTree()
|
||||
ORDER BY (project_id, event_type, date)
|
||||
AS SELECT
|
||||
project_id,
|
||||
event_type,
|
||||
toDate(timestamp) AS date,
|
||||
SUM(metric_value) AS value,
|
||||
COUNT(*) AS count
|
||||
FROM events
|
||||
WHERE project_id IS NOT NULL AND event_type = 'project_comment'
|
||||
GROUP BY project_id, event_type, date
|
||||
`
|
||||
});
|
||||
|
||||
console.log('ClickHouse 数据表初始化完成');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('初始化 ClickHouse 数据表失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化 Supabase 存储函数
|
||||
*/
|
||||
export const initSupabaseFunctions = async () => {
|
||||
try {
|
||||
console.log('开始初始化 Supabase 存储过程...');
|
||||
|
||||
// 创建用户简档表的存储过程
|
||||
await supabase.rpc('create_function_create_user_profiles_if_not_exists');
|
||||
|
||||
// 创建项目表的存储过程
|
||||
await supabase.rpc('create_function_create_projects_table_if_not_exists');
|
||||
|
||||
// 创建网红表的存储过程
|
||||
await supabase.rpc('create_function_create_influencers_table_if_not_exists');
|
||||
|
||||
// 创建项目-网红关联表的存储过程
|
||||
await supabase.rpc('create_function_create_project_influencers_table_if_not_exists');
|
||||
|
||||
// 创建帖子表的存储过程
|
||||
await supabase.rpc('create_function_create_posts_table_if_not_exists');
|
||||
|
||||
// 创建评论表的存储过程
|
||||
await supabase.rpc('create_function_create_comments_table_if_not_exists');
|
||||
|
||||
// 创建项目评论表的存储过程
|
||||
await supabase.rpc('create_function_create_project_comments_table_if_not_exists');
|
||||
|
||||
// 创建评论相关的SQL函数
|
||||
console.log('创建评论相关的SQL函数...');
|
||||
const commentsSQL = await fs.readFile(
|
||||
path.join(__dirname, 'supabase-comments-functions.sql'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
// 使用Supabase执行SQL
|
||||
const { error: commentsFunctionsError } = await supabase.rpc(
|
||||
'pgclient_execute',
|
||||
{ query: commentsSQL }
|
||||
);
|
||||
|
||||
if (commentsFunctionsError) {
|
||||
console.error('创建评论SQL函数失败:', commentsFunctionsError);
|
||||
} else {
|
||||
console.log('评论SQL函数创建成功');
|
||||
}
|
||||
|
||||
console.log('Supabase 存储过程初始化完成');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('初始化 Supabase 存储过程失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建测试数据
|
||||
*/
|
||||
export const createSampleData = async () => {
|
||||
try {
|
||||
console.log('开始创建测试数据...');
|
||||
|
||||
// 创建测试用户
|
||||
const { data: user, error: userError } = await supabase.auth.admin.createUser({
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
user_metadata: {
|
||||
full_name: '测试用户'
|
||||
}
|
||||
});
|
||||
|
||||
if (userError) {
|
||||
console.error('创建测试用户失败:', userError);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建测试项目
|
||||
const { data: project, error: projectError } = await supabase
|
||||
.from('projects')
|
||||
.insert({
|
||||
name: '测试营销活动',
|
||||
description: '这是一个测试营销活动',
|
||||
created_by: user.user.id
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (projectError) {
|
||||
console.error('创建测试项目失败:', projectError);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建项目评论
|
||||
await supabase
|
||||
.from('project_comments')
|
||||
.insert([
|
||||
{
|
||||
project_id: project.id,
|
||||
user_id: user.user.id,
|
||||
content: '这是对项目的一条测试评论',
|
||||
sentiment_score: 0.8
|
||||
},
|
||||
{
|
||||
project_id: project.id,
|
||||
user_id: user.user.id,
|
||||
content: '这个项目很有前景',
|
||||
sentiment_score: 0.9
|
||||
},
|
||||
{
|
||||
project_id: project.id,
|
||||
user_id: user.user.id,
|
||||
content: '需要关注这个项目的进展',
|
||||
sentiment_score: 0.7
|
||||
}
|
||||
]);
|
||||
|
||||
// 创建测试网红
|
||||
const platforms = ['youtube', 'instagram', 'tiktok'];
|
||||
const influencers = [];
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const platform = platforms[Math.floor(Math.random() * platforms.length)];
|
||||
|
||||
const { data: influencer, error: influencerError } = await supabase
|
||||
.from('influencers')
|
||||
.insert({
|
||||
name: `测试网红 ${i}`,
|
||||
platform,
|
||||
profile_url: `https://${platform}.com/user${i}`,
|
||||
external_id: `user_${platform}_${i}`,
|
||||
followers_count: Math.floor(Math.random() * 1000000) + 1000,
|
||||
video_count: Math.floor(Math.random() * 500) + 10
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (influencerError) {
|
||||
console.error(`创建测试网红 ${i} 失败:`, influencerError);
|
||||
continue;
|
||||
}
|
||||
|
||||
influencers.push(influencer);
|
||||
|
||||
// 将网红添加到项目
|
||||
await supabase
|
||||
.from('project_influencers')
|
||||
.insert({
|
||||
project_id: project.id,
|
||||
influencer_id: influencer.influencer_id
|
||||
});
|
||||
|
||||
// 为每个网红创建 3-5 个帖子
|
||||
const postCount = Math.floor(Math.random() * 3) + 3;
|
||||
|
||||
for (let j = 1; j <= postCount; j++) {
|
||||
const { data: post, error: postError } = await supabase
|
||||
.from('posts')
|
||||
.insert({
|
||||
influencer_id: influencer.influencer_id,
|
||||
platform,
|
||||
post_url: `https://${platform}.com/user${i}/post${j}`,
|
||||
title: `测试帖子 ${j} - 由 ${influencer.name} 发布`,
|
||||
description: `这是一个测试帖子的描述 ${j}`,
|
||||
published_at: new Date(Date.now() - Math.floor(Math.random() * 30) * 24 * 60 * 60 * 1000).toISOString()
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (postError) {
|
||||
console.error(`创建测试帖子 ${j} 失败:`, postError);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 为每个帖子创建 2-10 个评论
|
||||
const commentCount = Math.floor(Math.random() * 9) + 2;
|
||||
|
||||
for (let k = 1; k <= commentCount; k++) {
|
||||
await supabase
|
||||
.from('comments')
|
||||
.insert({
|
||||
post_id: post.post_id,
|
||||
user_id: user.user.id,
|
||||
content: `这是对帖子 ${post.title} 的测试评论 ${k}`,
|
||||
sentiment_score: (Math.random() * 2 - 1) // -1 到 1 之间的随机数
|
||||
});
|
||||
}
|
||||
|
||||
// 创建 ClickHouse 事件数据
|
||||
// 粉丝变化事件
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
INSERT INTO events (
|
||||
project_id,
|
||||
influencer_id,
|
||||
platform,
|
||||
event_type,
|
||||
metric_value,
|
||||
event_metadata
|
||||
) VALUES (?, ?, ?, 'follower_change', ?, ?)
|
||||
`,
|
||||
values: [
|
||||
project.id,
|
||||
influencer.influencer_id,
|
||||
platform,
|
||||
Math.floor(Math.random() * 1000) - 200, // -200 到 800 之间的随机数
|
||||
JSON.stringify({ source: 'api_crawler' })
|
||||
]
|
||||
});
|
||||
|
||||
// 帖子点赞变化事件
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
INSERT INTO events (
|
||||
project_id,
|
||||
influencer_id,
|
||||
post_id,
|
||||
platform,
|
||||
event_type,
|
||||
metric_value,
|
||||
event_metadata
|
||||
) VALUES (?, ?, ?, ?, 'post_like_change', ?, ?)
|
||||
`,
|
||||
values: [
|
||||
project.id,
|
||||
influencer.influencer_id,
|
||||
post.post_id,
|
||||
platform,
|
||||
Math.floor(Math.random() * 500) + 10, // 10 到 510 之间的随机数
|
||||
JSON.stringify({ source: 'api_crawler' })
|
||||
]
|
||||
});
|
||||
|
||||
// 帖子观看数变化事件
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
INSERT INTO events (
|
||||
project_id,
|
||||
influencer_id,
|
||||
post_id,
|
||||
platform,
|
||||
event_type,
|
||||
metric_value,
|
||||
event_metadata
|
||||
) VALUES (?, ?, ?, ?, 'post_view_change', ?, ?)
|
||||
`,
|
||||
values: [
|
||||
project.id,
|
||||
influencer.influencer_id,
|
||||
post.post_id,
|
||||
platform,
|
||||
Math.floor(Math.random() * 5000) + 100, // 100 到 5100 之间的随机数
|
||||
JSON.stringify({ source: 'api_crawler' })
|
||||
]
|
||||
});
|
||||
|
||||
// 互动事件
|
||||
const interactionTypes = ['click', 'comment', 'share'];
|
||||
const interactionType = interactionTypes[Math.floor(Math.random() * interactionTypes.length)];
|
||||
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
INSERT INTO events (
|
||||
project_id,
|
||||
influencer_id,
|
||||
post_id,
|
||||
platform,
|
||||
event_type,
|
||||
metric_value,
|
||||
event_metadata
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
values: [
|
||||
project.id,
|
||||
influencer.influencer_id,
|
||||
post.post_id,
|
||||
platform,
|
||||
interactionType,
|
||||
1,
|
||||
JSON.stringify({
|
||||
ip: '192.168.1.' + Math.floor(Math.random() * 255),
|
||||
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建项目评论事件
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
await clickhouse.query({
|
||||
query: `
|
||||
INSERT INTO events (
|
||||
project_id,
|
||||
event_type,
|
||||
metric_value,
|
||||
event_metadata
|
||||
) VALUES (?, 'project_comment', ?, ?)
|
||||
`,
|
||||
values: [
|
||||
project.id,
|
||||
1,
|
||||
JSON.stringify({
|
||||
user_id: user.user.id,
|
||||
timestamp: new Date().toISOString(),
|
||||
comment: `项目评论事件 ${i}`
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
console.log('测试数据创建完成');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('创建测试数据失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查数据库连接
|
||||
*/
|
||||
export const checkDatabaseConnection = async () => {
|
||||
try {
|
||||
console.log('检查数据库连接...');
|
||||
|
||||
// 检查 Supabase 连接
|
||||
try {
|
||||
// 仅检查连接是否正常,不执行实际查询
|
||||
const { data, error } = await supabase.auth.getSession();
|
||||
if (error) {
|
||||
console.error('Supabase 连接测试失败:', error);
|
||||
return false;
|
||||
}
|
||||
console.log('Supabase 连接正常');
|
||||
} catch (supabaseError) {
|
||||
console.error('Supabase 连接测试失败:', supabaseError);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查 ClickHouse 连接
|
||||
try {
|
||||
// 使用简单查询代替ping方法
|
||||
const result = await clickhouse.query({ query: 'SELECT 1' });
|
||||
console.log('ClickHouse 连接正常');
|
||||
} catch (error) {
|
||||
console.error('ClickHouse 连接测试失败:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('数据库连接检查完成,所有连接均正常');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('数据库连接检查失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化数据库 - 此函数现在仅作为手动初始化的入口点
|
||||
* 只有通过管理API明确调用时才会执行实际的初始化
|
||||
*/
|
||||
export const initDatabase = async () => {
|
||||
try {
|
||||
console.log('开始数据库初始化...');
|
||||
console.log('警告: 此操作将修改数据库结构,请确保您知道自己在做什么');
|
||||
|
||||
// 初始化 Supabase 函数
|
||||
await initSupabaseFunctions();
|
||||
|
||||
// 初始化 Supabase 表
|
||||
await initSupabaseTables();
|
||||
|
||||
// 初始化 ClickHouse 表
|
||||
await initClickHouseTables();
|
||||
|
||||
console.log('数据库初始化完成');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('数据库初始化失败:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
189
backend/src/utils/queue.ts
Normal file
189
backend/src/utils/queue.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { Queue, Worker, Job } from 'bullmq';
|
||||
import config from '../config';
|
||||
|
||||
// Define queue names
|
||||
export const QUEUE_NAMES = {
|
||||
ANALYTICS: 'analytics',
|
||||
NOTIFICATIONS: 'notifications',
|
||||
};
|
||||
|
||||
// Define job data types
|
||||
interface AnalyticsJobData {
|
||||
type: 'process_views' | 'process_likes' | 'process_followers';
|
||||
data: Record<string, any>;
|
||||
}
|
||||
|
||||
interface NotificationJobData {
|
||||
type: 'new_follower' | 'new_like';
|
||||
data: Record<string, any>;
|
||||
}
|
||||
|
||||
// Create Redis connection options
|
||||
const redisOptions = {
|
||||
host: config.bull.redis.host,
|
||||
port: config.bull.redis.port,
|
||||
password: config.bull.redis.password,
|
||||
};
|
||||
|
||||
// Create queues with error handling
|
||||
let analyticsQueue: Queue<AnalyticsJobData>;
|
||||
let notificationsQueue: Queue<NotificationJobData>;
|
||||
|
||||
try {
|
||||
analyticsQueue = new Queue<AnalyticsJobData>(QUEUE_NAMES.ANALYTICS, {
|
||||
connection: redisOptions,
|
||||
defaultJobOptions: {
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 1000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
notificationsQueue = new Queue<NotificationJobData>(QUEUE_NAMES.NOTIFICATIONS, {
|
||||
connection: redisOptions,
|
||||
defaultJobOptions: {
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 1000,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error initializing BullMQ queues:', error);
|
||||
// Create mock queues for development
|
||||
analyticsQueue = {
|
||||
add: async (name: string, data: AnalyticsJobData) => {
|
||||
console.log(`Mock analytics job added: ${name}`, data);
|
||||
return { id: 'mock-job-id' } as any;
|
||||
},
|
||||
close: async () => console.log('Mock analytics queue closed'),
|
||||
} as any;
|
||||
|
||||
notificationsQueue = {
|
||||
add: async (name: string, data: NotificationJobData) => {
|
||||
console.log(`Mock notification job added: ${name}`, data);
|
||||
return { id: 'mock-job-id' } as any;
|
||||
},
|
||||
close: async () => console.log('Mock notifications queue closed'),
|
||||
} as any;
|
||||
}
|
||||
|
||||
// Initialize workers
|
||||
export const initWorkers = () => {
|
||||
try {
|
||||
// Analytics worker
|
||||
const analyticsWorker = new Worker<AnalyticsJobData>(
|
||||
QUEUE_NAMES.ANALYTICS,
|
||||
async (job: Job<AnalyticsJobData>) => {
|
||||
console.log(`Processing analytics job ${job.id}`);
|
||||
const { type, data } = job.data;
|
||||
|
||||
switch (type) {
|
||||
case 'process_views':
|
||||
// Process view analytics
|
||||
console.log('Processing view analytics', data);
|
||||
break;
|
||||
case 'process_likes':
|
||||
// Process like analytics
|
||||
console.log('Processing like analytics', data);
|
||||
break;
|
||||
case 'process_followers':
|
||||
// Process follower analytics
|
||||
console.log('Processing follower analytics', data);
|
||||
break;
|
||||
default:
|
||||
console.log(`Unknown analytics job type: ${type as string}`);
|
||||
}
|
||||
},
|
||||
{ connection: redisOptions }
|
||||
);
|
||||
|
||||
// Notifications worker
|
||||
const notificationsWorker = new Worker<NotificationJobData>(
|
||||
QUEUE_NAMES.NOTIFICATIONS,
|
||||
async (job: Job<NotificationJobData>) => {
|
||||
console.log(`Processing notification job ${job.id}`);
|
||||
const { type, data } = job.data;
|
||||
|
||||
switch (type) {
|
||||
case 'new_follower':
|
||||
// Send new follower notification
|
||||
console.log('Sending new follower notification', data);
|
||||
break;
|
||||
case 'new_like':
|
||||
// Send new like notification
|
||||
console.log('Sending new like notification', data);
|
||||
break;
|
||||
default:
|
||||
console.log(`Unknown notification job type: ${type as string}`);
|
||||
}
|
||||
},
|
||||
{ connection: redisOptions }
|
||||
);
|
||||
|
||||
// Handle worker events
|
||||
analyticsWorker.on('completed', (job: Job<AnalyticsJobData>) => {
|
||||
console.log(`Analytics job ${job.id} completed`);
|
||||
});
|
||||
|
||||
analyticsWorker.on('failed', (job: Job<AnalyticsJobData> | undefined, err: Error) => {
|
||||
console.error(`Analytics job ${job?.id} failed with error ${err.message}`);
|
||||
});
|
||||
|
||||
notificationsWorker.on('completed', (job: Job<NotificationJobData>) => {
|
||||
console.log(`Notification job ${job.id} completed`);
|
||||
});
|
||||
|
||||
notificationsWorker.on('failed', (job: Job<NotificationJobData> | undefined, err: Error) => {
|
||||
console.error(`Notification job ${job?.id} failed with error ${err.message}`);
|
||||
});
|
||||
|
||||
return {
|
||||
analyticsWorker,
|
||||
notificationsWorker,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error initializing BullMQ workers:', error);
|
||||
// Return mock workers
|
||||
return {
|
||||
analyticsWorker: {
|
||||
close: async () => console.log('Mock analytics worker closed'),
|
||||
},
|
||||
notificationsWorker: {
|
||||
close: async () => console.log('Mock notifications worker closed'),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to add jobs to queues
|
||||
export const addAnalyticsJob = async (
|
||||
type: AnalyticsJobData['type'],
|
||||
data: Record<string, any>,
|
||||
options = {}
|
||||
) => {
|
||||
try {
|
||||
return await analyticsQueue.add(type, { type, data } as AnalyticsJobData, options);
|
||||
} catch (error) {
|
||||
console.error('Error adding analytics job:', error);
|
||||
console.log('Job details:', { type, data });
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const addNotificationJob = async (
|
||||
type: NotificationJobData['type'],
|
||||
data: Record<string, any>,
|
||||
options = {}
|
||||
) => {
|
||||
try {
|
||||
return await notificationsQueue.add(type, { type, data } as NotificationJobData, options);
|
||||
} catch (error) {
|
||||
console.error('Error adding notification job:', error);
|
||||
console.log('Job details:', { type, data });
|
||||
return null;
|
||||
}
|
||||
};
|
||||
80
backend/src/utils/redis.ts
Normal file
80
backend/src/utils/redis.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { createClient } from 'redis';
|
||||
import config from '../config';
|
||||
|
||||
// Create Redis client
|
||||
const redisClient = createClient({
|
||||
url: `redis://${config.redis.password ? `${config.redis.password}@` : ''}${config.redis.host}:${config.redis.port}`,
|
||||
});
|
||||
|
||||
// Handle Redis connection errors
|
||||
redisClient.on('error', (err) => {
|
||||
console.error('Redis Client Error:', err);
|
||||
});
|
||||
|
||||
// Create a mock Redis client for development when real connection fails
|
||||
const createMockRedisClient = () => {
|
||||
const store = new Map<string, string>();
|
||||
|
||||
return {
|
||||
isOpen: true,
|
||||
connect: async () => console.log('Mock Redis client connected'),
|
||||
get: async (key: string) => store.get(key) || null,
|
||||
set: async (key: string, value: string) => {
|
||||
store.set(key, value);
|
||||
return 'OK';
|
||||
},
|
||||
incr: async (key: string) => {
|
||||
const current = parseInt(store.get(key) || '0', 10);
|
||||
const newValue = current + 1;
|
||||
store.set(key, newValue.toString());
|
||||
return newValue;
|
||||
},
|
||||
decr: async (key: string) => {
|
||||
const current = parseInt(store.get(key) || '0', 10);
|
||||
const newValue = Math.max(0, current - 1);
|
||||
store.set(key, newValue.toString());
|
||||
return newValue;
|
||||
},
|
||||
quit: async () => console.log('Mock Redis client disconnected'),
|
||||
};
|
||||
};
|
||||
|
||||
// Connect to Redis
|
||||
let mockRedisClient: ReturnType<typeof createMockRedisClient> | null = null;
|
||||
|
||||
const connectRedis = async () => {
|
||||
try {
|
||||
if (!redisClient.isOpen) {
|
||||
await redisClient.connect();
|
||||
console.log('Redis client connected');
|
||||
}
|
||||
return redisClient;
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to Redis:', error);
|
||||
console.log('Using mock Redis client for development...');
|
||||
|
||||
if (!mockRedisClient) {
|
||||
mockRedisClient = createMockRedisClient();
|
||||
}
|
||||
|
||||
return mockRedisClient;
|
||||
}
|
||||
};
|
||||
|
||||
// Export the appropriate client
|
||||
const getRedisClient = async () => {
|
||||
try {
|
||||
if (redisClient.isOpen) {
|
||||
return redisClient;
|
||||
}
|
||||
return await connectRedis();
|
||||
} catch (error) {
|
||||
if (!mockRedisClient) {
|
||||
mockRedisClient = createMockRedisClient();
|
||||
}
|
||||
return mockRedisClient;
|
||||
}
|
||||
};
|
||||
|
||||
export { redisClient, connectRedis, getRedisClient };
|
||||
export default redisClient;
|
||||
97
backend/src/utils/supabase-comments-functions.sql
Normal file
97
backend/src/utils/supabase-comments-functions.sql
Normal file
@@ -0,0 +1,97 @@
|
||||
-- 创建获取所有评论的函数
|
||||
CREATE
|
||||
OR REPLACE FUNCTION public .get_comments_with_posts() RETURNS TABLE (
|
||||
comment_id UUID,
|
||||
content TEXT,
|
||||
sentiment_score FLOAT,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE,
|
||||
post_id UUID,
|
||||
user_id UUID,
|
||||
user_profile JSONB,
|
||||
post JSONB
|
||||
) LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
c .comment_id,
|
||||
c .content,
|
||||
c .sentiment_score,
|
||||
c .created_at,
|
||||
c .updated_at,
|
||||
c .post_id,
|
||||
c .user_id,
|
||||
jsonb_build_object(
|
||||
'id',
|
||||
up.id,
|
||||
'full_name',
|
||||
up.full_name,
|
||||
'avatar_url',
|
||||
up.avatar_url
|
||||
) AS user_profile,
|
||||
jsonb_build_object(
|
||||
'post_id',
|
||||
p.post_id,
|
||||
'title',
|
||||
p.title,
|
||||
'description',
|
||||
p.description,
|
||||
'platform',
|
||||
p.platform,
|
||||
'post_url',
|
||||
p.post_url,
|
||||
'published_at',
|
||||
p.published_at,
|
||||
'influencer_id',
|
||||
p.influencer_id
|
||||
) AS post
|
||||
FROM
|
||||
public .comments c
|
||||
LEFT JOIN public .user_profiles up ON c .user_id = up.id
|
||||
LEFT JOIN public .posts p ON c .post_id = p.post_id
|
||||
ORDER BY
|
||||
c .created_at DESC;
|
||||
|
||||
END;
|
||||
|
||||
$$;
|
||||
|
||||
-- 创建获取特定帖子评论的函数
|
||||
CREATE
|
||||
OR REPLACE FUNCTION public .get_comments_for_post(post_id_param UUID) RETURNS TABLE (
|
||||
comment_id UUID,
|
||||
content TEXT,
|
||||
sentiment_score FLOAT,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE,
|
||||
post_id UUID,
|
||||
user_id UUID,
|
||||
user_profile JSONB
|
||||
) LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
c .comment_id,
|
||||
c .content,
|
||||
c .sentiment_score,
|
||||
c .created_at,
|
||||
c .updated_at,
|
||||
c .post_id,
|
||||
c .user_id,
|
||||
jsonb_build_object(
|
||||
'id',
|
||||
up.id,
|
||||
'full_name',
|
||||
up.full_name,
|
||||
'avatar_url',
|
||||
up.avatar_url
|
||||
) AS user_profile
|
||||
FROM
|
||||
public .comments c
|
||||
LEFT JOIN public .user_profiles up ON c .user_id = up.id
|
||||
WHERE
|
||||
c .post_id = post_id_param
|
||||
ORDER BY
|
||||
c .created_at DESC;
|
||||
|
||||
END;
|
||||
|
||||
$$;
|
||||
738
backend/src/utils/supabase-functions.sql
Normal file
738
backend/src/utils/supabase-functions.sql
Normal file
@@ -0,0 +1,738 @@
|
||||
-- 为系统创建所需的存储过程和表
|
||||
-- 创建用户简档表的函数
|
||||
CREATE
|
||||
OR REPLACE FUNCTION create_function_create_user_profiles_if_not_exists() RETURNS void LANGUAGE plpgsql AS $$ BEGIN
|
||||
EXECUTE $FUNC$ CREATE
|
||||
OR REPLACE FUNCTION create_user_profiles_if_not_exists() RETURNS void LANGUAGE plpgsql AS $INNER$ BEGIN
|
||||
-- 检查用户简档表是否存在
|
||||
IF NOT EXISTS (
|
||||
SELECT
|
||||
FROM
|
||||
pg_tables
|
||||
WHERE
|
||||
schemaname = 'public'
|
||||
AND tablename = 'user_profiles'
|
||||
) THEN -- 创建用户简档表
|
||||
CREATE TABLE public .user_profiles (
|
||||
id UUID PRIMARY KEY REFERENCES auth.users(id) ON
|
||||
DELETE
|
||||
CASCADE,
|
||||
full_name TEXT,
|
||||
avatar_url TEXT,
|
||||
website TEXT,
|
||||
company TEXT,
|
||||
role TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
-- 添加 RLS 策略
|
||||
ALTER TABLE
|
||||
public .user_profiles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 创建只有自己可以更新自己简档的策略
|
||||
CREATE POLICY "Users can view all profiles" ON public .user_profiles FOR
|
||||
SELECT
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY "Users can update own profile" ON public .user_profiles FOR
|
||||
UPDATE
|
||||
USING (auth.uid() = id);
|
||||
|
||||
CREATE POLICY "Users can insert own profile" ON public .user_profiles FOR
|
||||
INSERT
|
||||
WITH CHECK (auth.uid() = id);
|
||||
|
||||
-- 创建新用户时自动创建简档的触发器
|
||||
CREATE
|
||||
OR REPLACE FUNCTION public .handle_new_user() RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER AS $TRIGGER$ BEGIN
|
||||
INSERT INTO
|
||||
public .user_profiles (id, full_name, avatar_url)
|
||||
VALUES
|
||||
(
|
||||
NEW .id,
|
||||
NEW .raw_user_meta_data ->> 'full_name',
|
||||
NEW .raw_user_meta_data ->> 'avatar_url'
|
||||
);
|
||||
|
||||
RETURN NEW;
|
||||
|
||||
END;
|
||||
|
||||
$TRIGGER$;
|
||||
|
||||
-- 创建触发器
|
||||
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
||||
|
||||
CREATE TRIGGER on_auth_user_created AFTER
|
||||
INSERT
|
||||
ON auth.users FOR EACH ROW EXECUTE FUNCTION public .handle_new_user();
|
||||
|
||||
RAISE NOTICE 'Created user_profiles table with RLS policies and triggers';
|
||||
|
||||
ELSE RAISE NOTICE 'user_profiles table already exists';
|
||||
|
||||
END IF;
|
||||
|
||||
END;
|
||||
|
||||
$INNER$;
|
||||
|
||||
$FUNC$;
|
||||
|
||||
RAISE NOTICE 'Created function create_user_profiles_if_not_exists()';
|
||||
|
||||
END;
|
||||
|
||||
$$;
|
||||
|
||||
-- 创建项目表的函数
|
||||
CREATE
|
||||
OR REPLACE FUNCTION create_function_create_projects_table_if_not_exists() RETURNS void LANGUAGE plpgsql AS $$ BEGIN
|
||||
EXECUTE $FUNC$ CREATE
|
||||
OR REPLACE FUNCTION create_projects_table_if_not_exists() RETURNS void LANGUAGE plpgsql AS $INNER$ BEGIN
|
||||
-- 检查项目表是否存在
|
||||
IF NOT EXISTS (
|
||||
SELECT
|
||||
FROM
|
||||
pg_tables
|
||||
WHERE
|
||||
schemaname = 'public'
|
||||
AND tablename = 'projects'
|
||||
) THEN -- 创建项目表
|
||||
CREATE TABLE public .projects (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'archived', 'completed')),
|
||||
start_date TIMESTAMP WITH TIME ZONE,
|
||||
end_date TIMESTAMP WITH TIME ZONE,
|
||||
created_by UUID REFERENCES auth.users(id) ON
|
||||
DELETE
|
||||
SET
|
||||
NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
-- 添加 RLS 策略
|
||||
ALTER TABLE
|
||||
public .projects ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 创建只有创建者可以管理项目的策略
|
||||
CREATE POLICY "Users can view own projects" ON public .projects FOR
|
||||
SELECT
|
||||
USING (auth.uid() = created_by);
|
||||
|
||||
CREATE POLICY "Users can insert own projects" ON public .projects FOR
|
||||
INSERT
|
||||
WITH CHECK (auth.uid() = created_by);
|
||||
|
||||
CREATE POLICY "Users can update own projects" ON public .projects FOR
|
||||
UPDATE
|
||||
USING (auth.uid() = created_by);
|
||||
|
||||
CREATE POLICY "Users can delete own projects" ON public .projects FOR
|
||||
DELETE
|
||||
USING (auth.uid() = created_by);
|
||||
|
||||
-- 创建更新时间的触发器
|
||||
CREATE
|
||||
OR REPLACE FUNCTION public .update_project_updated_at() RETURNS TRIGGER LANGUAGE plpgsql AS $TRIGGER$ BEGIN
|
||||
NEW .updated_at = now();
|
||||
|
||||
RETURN NEW;
|
||||
|
||||
END;
|
||||
|
||||
$TRIGGER$;
|
||||
|
||||
-- 创建触发器
|
||||
DROP TRIGGER IF EXISTS on_project_updated ON public .projects;
|
||||
|
||||
CREATE TRIGGER on_project_updated BEFORE
|
||||
UPDATE
|
||||
ON public .projects FOR EACH ROW EXECUTE FUNCTION public .update_project_updated_at();
|
||||
|
||||
RAISE NOTICE 'Created projects table with RLS policies and triggers';
|
||||
|
||||
ELSE RAISE NOTICE 'projects table already exists';
|
||||
|
||||
END IF;
|
||||
|
||||
END;
|
||||
|
||||
$INNER$;
|
||||
|
||||
$FUNC$;
|
||||
|
||||
RAISE NOTICE 'Created function create_projects_table_if_not_exists()';
|
||||
|
||||
END;
|
||||
|
||||
$$;
|
||||
|
||||
-- 创建网红(影响者)表的函数
|
||||
CREATE
|
||||
OR REPLACE FUNCTION create_function_create_influencers_table_if_not_exists() RETURNS void LANGUAGE plpgsql AS $$ BEGIN
|
||||
EXECUTE $FUNC$ CREATE
|
||||
OR REPLACE FUNCTION create_influencers_table_if_not_exists() RETURNS void LANGUAGE plpgsql AS $INNER$ BEGIN
|
||||
-- 检查网红表是否存在
|
||||
IF NOT EXISTS (
|
||||
SELECT
|
||||
FROM
|
||||
pg_tables
|
||||
WHERE
|
||||
schemaname = 'public'
|
||||
AND tablename = 'influencers'
|
||||
) THEN -- 创建网红表
|
||||
CREATE TABLE public .influencers (
|
||||
influencer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
platform TEXT CHECK (
|
||||
platform IN (
|
||||
'youtube',
|
||||
'instagram',
|
||||
'tiktok',
|
||||
'twitter',
|
||||
'facebook'
|
||||
)
|
||||
),
|
||||
profile_url TEXT,
|
||||
external_id TEXT UNIQUE,
|
||||
followers_count INT DEFAULT 0,
|
||||
video_count INT DEFAULT 0,
|
||||
platform_count INT DEFAULT 1,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
-- 添加 RLS 策略
|
||||
ALTER TABLE
|
||||
public .influencers ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 创建所有认证用户可以查看网红的策略
|
||||
CREATE POLICY "Authenticated users can view influencers" ON public .influencers FOR
|
||||
SELECT
|
||||
USING (auth.role() = 'authenticated');
|
||||
|
||||
-- 创建有权限的用户可以更新网红的策略
|
||||
CREATE POLICY "Authenticated users can insert influencers" ON public .influencers FOR
|
||||
INSERT
|
||||
WITH CHECK (auth.role() = 'authenticated');
|
||||
|
||||
CREATE POLICY "Authenticated users can update influencers" ON public .influencers FOR
|
||||
UPDATE
|
||||
USING (auth.role() = 'authenticated');
|
||||
|
||||
-- 创建更新时间的触发器
|
||||
CREATE
|
||||
OR REPLACE FUNCTION public .update_influencer_updated_at() RETURNS TRIGGER LANGUAGE plpgsql AS $TRIGGER$ BEGIN
|
||||
NEW .updated_at = now();
|
||||
|
||||
RETURN NEW;
|
||||
|
||||
END;
|
||||
|
||||
$TRIGGER$;
|
||||
|
||||
-- 创建触发器
|
||||
DROP TRIGGER IF EXISTS on_influencer_updated ON public .influencers;
|
||||
|
||||
CREATE TRIGGER on_influencer_updated BEFORE
|
||||
UPDATE
|
||||
ON public .influencers FOR EACH ROW EXECUTE FUNCTION public .update_influencer_updated_at();
|
||||
|
||||
RAISE NOTICE 'Created influencers table with RLS policies and triggers';
|
||||
|
||||
ELSE RAISE NOTICE 'influencers table already exists';
|
||||
|
||||
END IF;
|
||||
|
||||
END;
|
||||
|
||||
$INNER$;
|
||||
|
||||
$FUNC$;
|
||||
|
||||
RAISE NOTICE 'Created function create_influencers_table_if_not_exists()';
|
||||
|
||||
END;
|
||||
|
||||
$$;
|
||||
|
||||
-- 创建项目-网红关联表的函数
|
||||
CREATE
|
||||
OR REPLACE FUNCTION create_function_create_project_influencers_table_if_not_exists() RETURNS void LANGUAGE plpgsql AS $$ BEGIN
|
||||
EXECUTE $FUNC$ CREATE
|
||||
OR REPLACE FUNCTION create_project_influencers_table_if_not_exists() RETURNS void LANGUAGE plpgsql AS $INNER$ BEGIN
|
||||
-- 检查项目-网红关联表是否存在
|
||||
IF NOT EXISTS (
|
||||
SELECT
|
||||
FROM
|
||||
pg_tables
|
||||
WHERE
|
||||
schemaname = 'public'
|
||||
AND tablename = 'project_influencers'
|
||||
) THEN -- 创建项目-网红关联表
|
||||
CREATE TABLE public .project_influencers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id UUID REFERENCES public .projects(id) ON
|
||||
DELETE
|
||||
CASCADE,
|
||||
influencer_id UUID REFERENCES public .influencers(influencer_id) ON
|
||||
DELETE
|
||||
CASCADE,
|
||||
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'completed')),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
UNIQUE (project_id, influencer_id)
|
||||
);
|
||||
|
||||
-- 添加索引
|
||||
CREATE INDEX idx_project_influencers_project_id ON public .project_influencers(project_id);
|
||||
|
||||
CREATE INDEX idx_project_influencers_influencer_id ON public .project_influencers(influencer_id);
|
||||
|
||||
-- 添加 RLS 策略
|
||||
ALTER TABLE
|
||||
public .project_influencers ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 创建只有项目创建者可以管理项目-网红关联的策略
|
||||
CREATE POLICY "Users can view project influencers" ON public .project_influencers FOR
|
||||
SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
public .projects
|
||||
WHERE
|
||||
id = project_id
|
||||
AND created_by = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can insert project influencers" ON public .project_influencers FOR
|
||||
INSERT
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
public .projects
|
||||
WHERE
|
||||
id = project_id
|
||||
AND created_by = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can update project influencers" ON public .project_influencers FOR
|
||||
UPDATE
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
public .projects
|
||||
WHERE
|
||||
id = project_id
|
||||
AND created_by = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "Users can delete project influencers" ON public .project_influencers FOR
|
||||
DELETE
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
public .projects
|
||||
WHERE
|
||||
id = project_id
|
||||
AND created_by = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- 创建更新时间的触发器
|
||||
CREATE
|
||||
OR REPLACE FUNCTION public .update_project_influencer_updated_at() RETURNS TRIGGER LANGUAGE plpgsql AS $TRIGGER$ BEGIN
|
||||
NEW .updated_at = now();
|
||||
|
||||
RETURN NEW;
|
||||
|
||||
END;
|
||||
|
||||
$TRIGGER$;
|
||||
|
||||
-- 创建触发器
|
||||
DROP TRIGGER IF EXISTS on_project_influencer_updated ON public .project_influencers;
|
||||
|
||||
CREATE TRIGGER on_project_influencer_updated BEFORE
|
||||
UPDATE
|
||||
ON public .project_influencers FOR EACH ROW EXECUTE FUNCTION public .update_project_influencer_updated_at();
|
||||
|
||||
RAISE NOTICE 'Created project_influencers table with RLS policies and triggers';
|
||||
|
||||
ELSE RAISE NOTICE 'project_influencers table already exists';
|
||||
|
||||
END IF;
|
||||
|
||||
END;
|
||||
|
||||
$INNER$;
|
||||
|
||||
$FUNC$;
|
||||
|
||||
RAISE NOTICE 'Created function create_project_influencers_table_if_not_exists()';
|
||||
|
||||
END;
|
||||
|
||||
$$;
|
||||
|
||||
-- 创建帖子表的函数
|
||||
CREATE
|
||||
OR REPLACE FUNCTION create_function_create_posts_table_if_not_exists() RETURNS void LANGUAGE plpgsql AS $$ BEGIN
|
||||
EXECUTE $FUNC$ CREATE
|
||||
OR REPLACE FUNCTION create_posts_table_if_not_exists() RETURNS void LANGUAGE plpgsql AS $INNER$ BEGIN
|
||||
-- 检查帖子表是否存在
|
||||
IF NOT EXISTS (
|
||||
SELECT
|
||||
FROM
|
||||
pg_tables
|
||||
WHERE
|
||||
schemaname = 'public'
|
||||
AND tablename = 'posts'
|
||||
) THEN -- 创建帖子表
|
||||
CREATE TABLE public .posts (
|
||||
post_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
influencer_id UUID REFERENCES public .influencers(influencer_id) ON
|
||||
DELETE
|
||||
CASCADE,
|
||||
platform TEXT CHECK (
|
||||
platform IN (
|
||||
'youtube',
|
||||
'instagram',
|
||||
'tiktok',
|
||||
'twitter',
|
||||
'facebook'
|
||||
)
|
||||
),
|
||||
post_url TEXT UNIQUE NOT NULL,
|
||||
title TEXT,
|
||||
description TEXT,
|
||||
published_at TIMESTAMP WITH TIME ZONE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
-- 添加索引
|
||||
CREATE INDEX idx_posts_influencer_id ON public .posts(influencer_id);
|
||||
|
||||
CREATE INDEX idx_posts_platform ON public .posts(platform);
|
||||
|
||||
CREATE INDEX idx_posts_published_at ON public .posts(published_at);
|
||||
|
||||
-- 添加 RLS 策略
|
||||
ALTER TABLE
|
||||
public .posts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 创建所有认证用户可以查看帖子的策略
|
||||
CREATE POLICY "Authenticated users can view posts" ON public .posts FOR
|
||||
SELECT
|
||||
USING (auth.role() = 'authenticated');
|
||||
|
||||
-- 创建有权限的用户可以更新帖子的策略
|
||||
CREATE POLICY "Authenticated users can insert posts" ON public .posts FOR
|
||||
INSERT
|
||||
WITH CHECK (auth.role() = 'authenticated');
|
||||
|
||||
CREATE POLICY "Authenticated users can update posts" ON public .posts FOR
|
||||
UPDATE
|
||||
USING (auth.role() = 'authenticated');
|
||||
|
||||
-- 创建更新时间的触发器
|
||||
CREATE
|
||||
OR REPLACE FUNCTION public .update_post_updated_at() RETURNS TRIGGER LANGUAGE plpgsql AS $TRIGGER$ BEGIN
|
||||
NEW .updated_at = now();
|
||||
|
||||
RETURN NEW;
|
||||
|
||||
END;
|
||||
|
||||
$TRIGGER$;
|
||||
|
||||
-- 创建触发器
|
||||
DROP TRIGGER IF EXISTS on_post_updated ON public .posts;
|
||||
|
||||
CREATE TRIGGER on_post_updated BEFORE
|
||||
UPDATE
|
||||
ON public .posts FOR EACH ROW EXECUTE FUNCTION public .update_post_updated_at();
|
||||
|
||||
RAISE NOTICE 'Created posts table with RLS policies and triggers';
|
||||
|
||||
ELSE RAISE NOTICE 'posts table already exists';
|
||||
|
||||
END IF;
|
||||
|
||||
END;
|
||||
|
||||
$INNER$;
|
||||
|
||||
$FUNC$;
|
||||
|
||||
RAISE NOTICE 'Created function create_posts_table_if_not_exists()';
|
||||
|
||||
END;
|
||||
|
||||
$$;
|
||||
|
||||
-- 创建评论表的函数
|
||||
CREATE
|
||||
OR REPLACE FUNCTION create_function_create_comments_table_if_not_exists() RETURNS void LANGUAGE plpgsql AS $$ BEGIN
|
||||
EXECUTE $FUNC$ CREATE
|
||||
OR REPLACE FUNCTION create_comments_table_if_not_exists() RETURNS void LANGUAGE plpgsql AS $INNER$ BEGIN
|
||||
-- 检查评论表是否存在
|
||||
IF NOT EXISTS (
|
||||
SELECT
|
||||
FROM
|
||||
pg_tables
|
||||
WHERE
|
||||
schemaname = 'public'
|
||||
AND tablename = 'comments'
|
||||
) THEN -- 创建评论表
|
||||
CREATE TABLE public .comments (
|
||||
comment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
post_id UUID REFERENCES public .posts(post_id) ON
|
||||
DELETE
|
||||
CASCADE,
|
||||
user_id UUID REFERENCES auth.users(id) ON
|
||||
DELETE
|
||||
SET
|
||||
NULL,
|
||||
content TEXT NOT NULL,
|
||||
sentiment_score FLOAT,
|
||||
status TEXT DEFAULT 'approved' CHECK (status IN ('approved', 'pending', 'rejected')),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
-- 添加索引
|
||||
CREATE INDEX idx_comments_post_id ON public .comments(post_id);
|
||||
|
||||
CREATE INDEX idx_comments_user_id ON public .comments(user_id);
|
||||
|
||||
CREATE INDEX idx_comments_created_at ON public .comments(created_at);
|
||||
|
||||
-- 添加 RLS 策略
|
||||
ALTER TABLE
|
||||
public .comments ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 创建所有认证用户可以查看评论的策略
|
||||
CREATE POLICY "Authenticated users can view comments" ON public .comments FOR
|
||||
SELECT
|
||||
USING (auth.role() = 'authenticated');
|
||||
|
||||
-- 创建有权限的用户可以添加评论的策略
|
||||
CREATE POLICY "Authenticated users can insert comments" ON public .comments FOR
|
||||
INSERT
|
||||
WITH CHECK (auth.role() = 'authenticated');
|
||||
|
||||
-- 创建只有评论作者可以更新评论的策略
|
||||
CREATE POLICY "Users can update own comments" ON public .comments FOR
|
||||
UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- 创建只有评论作者可以删除评论的策略
|
||||
CREATE POLICY "Users can delete own comments" ON public .comments FOR
|
||||
DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- 创建更新时间的触发器
|
||||
CREATE
|
||||
OR REPLACE FUNCTION public .update_comment_updated_at() RETURNS TRIGGER LANGUAGE plpgsql AS $TRIGGER$ BEGIN
|
||||
NEW .updated_at = now();
|
||||
|
||||
RETURN NEW;
|
||||
|
||||
END;
|
||||
|
||||
$TRIGGER$;
|
||||
|
||||
-- 创建触发器
|
||||
DROP TRIGGER IF EXISTS on_comment_updated ON public .comments;
|
||||
|
||||
CREATE TRIGGER on_comment_updated BEFORE
|
||||
UPDATE
|
||||
ON public .comments FOR EACH ROW EXECUTE FUNCTION public .update_comment_updated_at();
|
||||
|
||||
RAISE NOTICE 'Created comments table with RLS policies and triggers';
|
||||
|
||||
ELSE RAISE NOTICE 'comments table already exists';
|
||||
|
||||
END IF;
|
||||
|
||||
END;
|
||||
|
||||
$INNER$;
|
||||
|
||||
$FUNC$;
|
||||
|
||||
RAISE NOTICE 'Created function create_comments_table_if_not_exists()';
|
||||
|
||||
END;
|
||||
|
||||
$$;
|
||||
|
||||
-- 创建项目评论表的函数
|
||||
CREATE
|
||||
OR REPLACE FUNCTION create_function_create_project_comments_table_if_not_exists() RETURNS void LANGUAGE plpgsql AS $$ BEGIN
|
||||
EXECUTE $FUNC$ CREATE
|
||||
OR REPLACE FUNCTION create_project_comments_table_if_not_exists() RETURNS void LANGUAGE plpgsql AS $INNER$ BEGIN
|
||||
-- 检查项目评论表是否存在
|
||||
IF NOT EXISTS (
|
||||
SELECT
|
||||
FROM
|
||||
pg_tables
|
||||
WHERE
|
||||
schemaname = 'public'
|
||||
AND tablename = 'project_comments'
|
||||
) THEN -- 创建项目评论表
|
||||
CREATE TABLE public .project_comments (
|
||||
comment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id UUID REFERENCES public .projects(id) ON
|
||||
DELETE
|
||||
CASCADE,
|
||||
user_id UUID REFERENCES auth.users(id) ON
|
||||
DELETE
|
||||
SET
|
||||
NULL,
|
||||
content TEXT NOT NULL,
|
||||
sentiment_score FLOAT,
|
||||
status TEXT DEFAULT 'approved' CHECK (status IN ('approved', 'pending', 'rejected')),
|
||||
is_pinned BOOLEAN DEFAULT false,
|
||||
parent_id UUID REFERENCES public .project_comments(comment_id) ON
|
||||
DELETE
|
||||
SET
|
||||
NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
-- 添加索引
|
||||
CREATE INDEX idx_project_comments_project_id ON public .project_comments(project_id);
|
||||
|
||||
CREATE INDEX idx_project_comments_user_id ON public .project_comments(user_id);
|
||||
|
||||
CREATE INDEX idx_project_comments_parent_id ON public .project_comments(parent_id);
|
||||
|
||||
CREATE INDEX idx_project_comments_created_at ON public .project_comments(created_at);
|
||||
|
||||
-- 添加 RLS 策略
|
||||
ALTER TABLE
|
||||
public .project_comments ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 创建项目评论可见性策略
|
||||
CREATE POLICY "Project members can view project comments" ON public .project_comments FOR
|
||||
SELECT
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
public .projects
|
||||
WHERE
|
||||
id = project_id
|
||||
AND (
|
||||
created_by = auth.uid()
|
||||
OR EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
public .project_influencers pi
|
||||
JOIN public .influencers i ON pi.influencer_id = i.influencer_id
|
||||
WHERE
|
||||
pi.project_id = project_id
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- 创建认证用户可以添加评论的策略
|
||||
CREATE POLICY "Authenticated users can insert project comments" ON public .project_comments FOR
|
||||
INSERT
|
||||
WITH CHECK (auth.role() = 'authenticated');
|
||||
|
||||
-- 创建只有评论作者可以更新评论的策略
|
||||
CREATE POLICY "Users can update own project comments" ON public .project_comments FOR
|
||||
UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- 创建项目所有者和评论作者可以删除评论的策略
|
||||
CREATE POLICY "Project owner and comment creator can delete project comments" ON public .project_comments FOR
|
||||
DELETE
|
||||
USING (
|
||||
auth.uid() = user_id
|
||||
OR EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
public .projects
|
||||
WHERE
|
||||
id = project_id
|
||||
AND created_by = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- 创建更新时间的触发器
|
||||
CREATE
|
||||
OR REPLACE FUNCTION public .update_project_comment_updated_at() RETURNS TRIGGER LANGUAGE plpgsql AS $TRIGGER$ BEGIN
|
||||
NEW .updated_at = now();
|
||||
|
||||
RETURN NEW;
|
||||
|
||||
END;
|
||||
|
||||
$TRIGGER$;
|
||||
|
||||
-- 创建触发器
|
||||
DROP TRIGGER IF EXISTS on_project_comment_updated ON public .project_comments;
|
||||
|
||||
CREATE TRIGGER on_project_comment_updated BEFORE
|
||||
UPDATE
|
||||
ON public .project_comments FOR EACH ROW EXECUTE FUNCTION public .update_project_comment_updated_at();
|
||||
|
||||
-- 创建获取评论回复数量的函数
|
||||
CREATE
|
||||
OR REPLACE FUNCTION public .get_reply_counts_for_comments(parent_ids UUID [ ]) RETURNS TABLE (parent_id UUID, count BIGINT) LANGUAGE sql SECURITY DEFINER AS $$
|
||||
SELECT
|
||||
parent_id,
|
||||
COUNT(*) as count
|
||||
FROM
|
||||
public .project_comments
|
||||
WHERE
|
||||
parent_id IS NOT NULL
|
||||
AND parent_id = ANY(parent_ids)
|
||||
GROUP BY
|
||||
parent_id;
|
||||
|
||||
$$;
|
||||
|
||||
RAISE NOTICE 'Created project_comments table with RLS policies and triggers';
|
||||
|
||||
ELSE RAISE NOTICE 'project_comments table already exists';
|
||||
|
||||
END IF;
|
||||
|
||||
END;
|
||||
|
||||
$INNER$;
|
||||
|
||||
$FUNC$;
|
||||
|
||||
RAISE NOTICE 'Created function create_project_comments_table_if_not_exists()';
|
||||
|
||||
END;
|
||||
|
||||
$$;
|
||||
19
backend/src/utils/supabase.ts
Normal file
19
backend/src/utils/supabase.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import config from '../config';
|
||||
|
||||
// Validate Supabase URL
|
||||
const validateSupabaseUrl = (url: string): string => {
|
||||
if (!url || !url.startsWith('http')) {
|
||||
console.warn('Invalid Supabase URL provided. Using a placeholder for development.');
|
||||
return 'https://example.supabase.co';
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
// Create a single supabase client for interacting with your database
|
||||
const supabase = createClient(
|
||||
validateSupabaseUrl(config.supabase.url),
|
||||
config.supabase.key || 'dummy-key'
|
||||
);
|
||||
|
||||
export default supabase;
|
||||
Reference in New Issue
Block a user