This commit is contained in:
2025-03-10 18:03:47 +08:00
parent 755fb6ac04
commit e49b3a2172
23 changed files with 802 additions and 5481 deletions

View File

@@ -1,51 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.config = void 0;
const dotenv_1 = __importDefault(require("dotenv"));
const path_1 = require("path");
// Load environment variables from .env file
dotenv_1.default.config({ path: (0, path_1.join)(__dirname, '../../.env') });
exports.config = {
port: process.env.PORT || 4000,
// Supabase configuration
supabase: {
url: process.env.SUPABASE_URL || '',
key: process.env.SUPABASE_KEY || '',
anonKey: process.env.SUPABASE_ANON_KEY || '',
},
// Redis configuration
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
password: process.env.REDIS_PASSWORD || '',
},
// ClickHouse configuration
clickhouse: {
host: process.env.CLICKHOUSE_HOST || 'localhost',
port: process.env.CLICKHOUSE_PORT || '8123',
user: process.env.CLICKHOUSE_USER || 'admin',
password: process.env.CLICKHOUSE_PASSWORD || 'your_secure_password',
database: process.env.CLICKHOUSE_DATABASE || 'promote',
},
// BullMQ configuration
bull: {
redis: {
host: process.env.BULL_REDIS_HOST || 'localhost',
port: parseInt(process.env.BULL_REDIS_PORT || '6379', 10),
password: process.env.BULL_REDIS_PASSWORD || '',
},
},
// JWT configuration
jwt: {
secret: process.env.JWT_SECRET || 'your-secret-key',
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
},
// Domain configuration
domain: process.env.DOMAIN || 'upj.to',
// Enabled routes
enabledRoutes: process.env.ENABLED_ROUTES || 'all',
};
exports.default = exports.config;

View File

@@ -1,111 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.deleteComment = exports.createComment = exports.getComments = void 0;
const supabase_1 = __importDefault(require("../utils/supabase"));
const getComments = async (c) => {
try {
const { post_id, limit = '10', offset = '0' } = c.req.query();
let query;
if (post_id) {
// 获取特定帖子的评论
query = supabase_1.default.rpc('get_comments_for_post', { post_id_param: post_id });
}
else {
// 获取所有评论
query = supabase_1.default.rpc('get_comments_with_posts');
}
// 应用分页
query = query.range(Number(offset), Number(offset) + Number(limit) - 1);
const { data: comments, error, count } = await query;
if (error) {
return c.json({ error: error.message }, 500);
}
return c.json({
comments,
count,
limit: Number(limit),
offset: Number(offset)
});
}
catch (error) {
return c.json({ error: 'Internal server error' }, 500);
}
};
exports.getComments = getComments;
const createComment = async (c) => {
try {
const { post_id, content } = await c.req.json();
const user_id = c.get('user')?.id;
if (!user_id) {
return c.json({ error: 'Unauthorized' }, 401);
}
const { data: comment, error } = await supabase_1.default
.from('comments')
.insert({
post_id,
content,
user_id
})
.select(`
comment_id,
content,
sentiment_score,
created_at,
updated_at,
post_id,
user_id
`)
.single();
if (error) {
return c.json({ error: error.message }, 500);
}
// 获取用户信息
const { data: userProfile, error: userError } = await supabase_1.default
.from('user_profiles')
.select('id, full_name, avatar_url')
.eq('id', user_id)
.single();
if (!userError && userProfile) {
comment.user_profile = userProfile;
}
return c.json(comment, 201);
}
catch (error) {
return c.json({ error: 'Internal server error' }, 500);
}
};
exports.createComment = createComment;
const deleteComment = async (c) => {
try {
const { comment_id } = c.req.param();
const user_id = c.get('user')?.id;
if (!user_id) {
return c.json({ error: 'Unauthorized' }, 401);
}
// Check if the comment belongs to the user
const { data: comment, error: fetchError } = await supabase_1.default
.from('comments')
.select()
.eq('comment_id', comment_id)
.eq('user_id', user_id)
.single();
if (fetchError || !comment) {
return c.json({ error: 'Comment not found or unauthorized' }, 404);
}
const { error: deleteError } = await supabase_1.default
.from('comments')
.delete()
.eq('comment_id', comment_id);
if (deleteError) {
return c.json({ error: deleteError.message }, 500);
}
return c.body(null, 204);
}
catch (error) {
return c.json({ error: 'Internal server error' }, 500);
}
};
exports.deleteComment = deleteComment;

View File

@@ -1,116 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getInfluencerStats = exports.getInfluencerById = exports.getInfluencers = void 0;
const supabase_1 = __importDefault(require("../utils/supabase"));
const getInfluencers = async (c) => {
try {
const { platform, limit = '10', offset = '0', min_followers, max_followers, sort_by = 'followers_count', sort_order = 'desc' } = c.req.query();
let query = supabase_1.default
.from('influencers')
.select(`
influencer_id,
name,
platform,
profile_url,
followers_count,
video_count,
platform_count,
created_at,
updated_at
`);
// Apply filters
if (platform) {
query = query.eq('platform', platform);
}
if (min_followers) {
query = query.gte('followers_count', Number(min_followers));
}
if (max_followers) {
query = query.lte('followers_count', Number(max_followers));
}
// Apply sorting
if (sort_by && ['followers_count', 'video_count', 'created_at'].includes(sort_by)) {
query = query.order(sort_by, { ascending: sort_order === 'asc' });
}
// Apply pagination
query = query.range(Number(offset), Number(offset) + Number(limit) - 1);
const { data: influencers, error, count } = await query;
if (error) {
return c.json({ error: error.message }, 500);
}
return c.json({
influencers,
count,
limit: Number(limit),
offset: Number(offset)
});
}
catch (error) {
return c.json({ error: 'Internal server error' }, 500);
}
};
exports.getInfluencers = getInfluencers;
const getInfluencerById = async (c) => {
try {
const { influencer_id } = c.req.param();
const { data: influencer, error } = await supabase_1.default
.from('influencers')
.select(`
influencer_id,
name,
platform,
profile_url,
followers_count,
video_count,
platform_count,
created_at,
updated_at,
posts (
post_id,
title,
description,
published_at
)
`)
.eq('influencer_id', influencer_id)
.single();
if (error) {
return c.json({ error: 'Influencer not found' }, 404);
}
return c.json(influencer);
}
catch (error) {
return c.json({ error: 'Internal server error' }, 500);
}
};
exports.getInfluencerById = getInfluencerById;
const getInfluencerStats = async (c) => {
try {
const { platform } = c.req.query();
let query = supabase_1.default
.from('influencers')
.select('platform, followers_count, video_count');
if (platform) {
query = query.eq('platform', platform);
}
const { data: stats, error } = await query;
if (error) {
return c.json({ error: error.message }, 500);
}
const aggregatedStats = {
total_influencers: stats.length,
total_followers: stats.reduce((sum, item) => sum + (item.followers_count || 0), 0),
total_videos: stats.reduce((sum, item) => sum + (item.video_count || 0), 0),
average_followers: Math.round(stats.reduce((sum, item) => sum + (item.followers_count || 0), 0) / (stats.length || 1)),
average_videos: Math.round(stats.reduce((sum, item) => sum + (item.video_count || 0), 0) / (stats.length || 1))
};
return c.json(aggregatedStats);
}
catch (error) {
return c.json({ error: 'Internal server error' }, 500);
}
};
exports.getInfluencerStats = getInfluencerStats;

163
backend/dist/index.js vendored
View File

@@ -1,163 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_server_1 = require("@hono/node-server");
const hono_1 = require("hono");
const cors_1 = require("hono/cors");
const logger_1 = require("hono/logger");
const config_1 = __importDefault(require("./config"));
const auth_1 = __importDefault(require("./routes/auth"));
const analytics_1 = __importDefault(require("./routes/analytics"));
const community_1 = __importDefault(require("./routes/community"));
const posts_1 = __importDefault(require("./routes/posts"));
const projectComments_1 = __importDefault(require("./routes/projectComments"));
const comments_1 = __importDefault(require("./routes/comments"));
const influencers_1 = __importDefault(require("./routes/influencers"));
const redis_1 = require("./utils/redis");
const clickhouse_1 = require("./utils/clickhouse");
const queue_1 = require("./utils/queue");
const initDatabase_1 = require("./utils/initDatabase");
const swagger_1 = require("./swagger");
// Create Hono app
const app = new hono_1.Hono();
// Middleware
app.use('*', (0, logger_1.logger)());
app.use('*', (0, cors_1.cors)({
origin: '*',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
exposeHeaders: ['Content-Length'],
maxAge: 86400,
}));
// Health check route
app.get('/', (c) => {
return c.json({
status: 'ok',
message: 'Promote API is running',
version: '1.0.0',
});
});
// 数据库初始化路由
app.post('/api/admin/init-db', async (c) => {
try {
const result = await (0, initDatabase_1.initDatabase)();
return c.json({
success: result,
message: result ? 'Database initialized successfully' : 'Database initialization failed'
});
}
catch (error) {
console.error('Error initializing database:', error);
return c.json({
success: false,
message: 'Error initializing database',
error: error instanceof Error ? error.message : String(error)
}, 500);
}
});
// 创建测试数据路由
app.post('/api/admin/create-sample-data', async (c) => {
try {
const result = await (0, initDatabase_1.createSampleData)();
return c.json({
success: result,
message: result ? 'Sample data created successfully' : 'Sample data creation failed'
});
}
catch (error) {
console.error('Error creating sample data:', error);
return c.json({
success: false,
message: 'Error creating sample data',
error: error instanceof Error ? error.message : String(error)
}, 500);
}
});
// Routes
app.route('/api/auth', auth_1.default);
app.route('/api/analytics', analytics_1.default);
app.route('/api/community', community_1.default);
app.route('/api/posts', posts_1.default);
app.route('/api/project-comments', projectComments_1.default);
app.route('/api/comments', comments_1.default);
app.route('/api/influencers', influencers_1.default);
// Swagger UI
const swaggerApp = (0, swagger_1.createSwaggerUI)();
app.route('', swaggerApp);
// Initialize services and start server
const startServer = async () => {
try {
// Connect to Redis
try {
await (0, redis_1.connectRedis)();
console.log('Connected to Redis');
}
catch (error) {
console.error('Failed to connect to Redis:', error);
console.log('Continuing with mock Redis client...');
}
// Initialize ClickHouse
try {
await (0, clickhouse_1.initClickHouse)();
console.log('ClickHouse initialized');
}
catch (error) {
console.error('Failed to initialize ClickHouse:', error);
console.log('Continuing with limited analytics functionality...');
}
// 检查数据库连接,但不自动初始化或修改数据库
try {
await (0, initDatabase_1.checkDatabaseConnection)();
}
catch (error) {
console.error('Database connection check failed:', error);
console.log('Some features may not work correctly if database is not properly set up');
}
console.log('NOTICE: Database will NOT be automatically initialized on startup');
console.log('Use /api/admin/init-db endpoint to manually initialize the database if needed');
// Initialize BullMQ workers
let workers;
try {
workers = (0, queue_1.initWorkers)();
console.log('BullMQ workers initialized');
}
catch (error) {
console.error('Failed to initialize BullMQ workers:', error);
console.log('Background processing will not be available...');
workers = { analyticsWorker: null, notificationsWorker: null };
}
// Start server
const port = Number(config_1.default.port);
console.log(`Server starting on port ${port}...`);
(0, node_server_1.serve)({
fetch: app.fetch,
port,
});
console.log(`Server running at http://localhost:${port}`);
console.log(`Swagger UI available at http://localhost:${port}/swagger`);
console.log(`Initialize database at http://localhost:${port}/api/admin/init-db (POST)`);
console.log(`Create sample data at http://localhost:${port}/api/admin/create-sample-data (POST)`);
// Handle graceful shutdown
const shutdown = async () => {
console.log('Shutting down server...');
// Close workers if they exist
if (workers.analyticsWorker) {
await workers.analyticsWorker.close();
}
if (workers.notificationsWorker) {
await workers.notificationsWorker.close();
}
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
}
catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
};
// Start the server
startServer();

View File

@@ -1,85 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifySupabaseToken = exports.generateToken = exports.authMiddleware = void 0;
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
const config_1 = __importDefault(require("../config"));
const supabase_1 = __importDefault(require("../utils/supabase"));
// Middleware to verify JWT token
const authMiddleware = async (c, next) => {
try {
// Get authorization header
const authHeader = c.req.header('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized: No token provided' }, 401);
}
// Extract token
const token = authHeader.split(' ')[1];
try {
// 验证 JWT token
const decoded = jsonwebtoken_1.default.verify(token, config_1.default.jwt.secret);
// 特殊处理 Swagger 测试 token
if (decoded.sub === 'swagger-test-user' && decoded.email === 'swagger@test.com') {
// 为 Swagger 测试设置一个模拟用户
c.set('user', {
id: 'swagger-test-user',
email: 'swagger@test.com',
name: 'Swagger Test User'
});
// 继续到下一个中间件或路由处理器
await next();
return;
}
// 设置用户信息到上下文
c.set('user', {
id: decoded.sub,
email: decoded.email
});
// 继续到下一个中间件或路由处理器
await next();
}
catch (jwtError) {
if (jwtError instanceof jsonwebtoken_1.default.JsonWebTokenError) {
return c.json({ error: 'Unauthorized: Invalid token' }, 401);
}
if (jwtError instanceof jsonwebtoken_1.default.TokenExpiredError) {
return c.json({ error: 'Unauthorized: Token expired' }, 401);
}
throw jwtError;
}
}
catch (error) {
console.error('Auth middleware error:', error);
return c.json({ error: 'Internal server error' }, 500);
}
};
exports.authMiddleware = authMiddleware;
// Generate JWT token
const generateToken = (userId, email) => {
const secret = config_1.default.jwt.secret;
const expiresIn = config_1.default.jwt.expiresIn;
return jsonwebtoken_1.default.sign({
sub: userId,
email,
}, secret, {
expiresIn,
});
};
exports.generateToken = generateToken;
// Verify Supabase token
const verifySupabaseToken = async (token) => {
try {
const { data, error } = await supabase_1.default.auth.getUser(token);
if (error || !data.user) {
return null;
}
return data.user;
}
catch (error) {
console.error('Supabase token verification error:', error);
return null;
}
};
exports.verifySupabaseToken = verifySupabaseToken;

View File

@@ -1,453 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const hono_1 = require("hono");
const auth_1 = require("../middlewares/auth");
const clickhouse_1 = __importDefault(require("../utils/clickhouse"));
const queue_1 = require("../utils/queue");
const redis_1 = require("../utils/redis");
const supabase_1 = __importDefault(require("../utils/supabase"));
const analyticsRouter = new hono_1.Hono();
// Apply auth middleware to all routes
analyticsRouter.use('*', auth_1.authMiddleware);
// Track a view event
analyticsRouter.post('/view', async (c) => {
try {
const { content_id } = await c.req.json();
const user = c.get('user');
if (!content_id) {
return c.json({ error: 'Content ID is required' }, 400);
}
// Get IP and user agent
const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || '0.0.0.0';
const userAgent = c.req.header('user-agent') || 'unknown';
// Insert view event into ClickHouse
await clickhouse_1.default.query({
query: `
INSERT INTO promote.view_events (user_id, content_id, ip, user_agent)
VALUES (?, ?, ?, ?)
`,
values: [
user.id,
content_id,
ip,
userAgent
]
});
// Queue analytics processing job
await (0, queue_1.addAnalyticsJob)('process_views', {
user_id: user.id,
content_id,
timestamp: new Date().toISOString()
});
// Increment view count in Redis cache
const redis = await (0, redis_1.getRedisClient)();
await redis.incr(`views:${content_id}`);
return c.json({ message: 'View tracked successfully' });
}
catch (error) {
console.error('View tracking error:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// Track a like event
analyticsRouter.post('/like', async (c) => {
try {
const { content_id, action } = await c.req.json();
const user = c.get('user');
if (!content_id || !action) {
return c.json({ error: 'Content ID and action are required' }, 400);
}
if (action !== 'like' && action !== 'unlike') {
return c.json({ error: 'Action must be either "like" or "unlike"' }, 400);
}
// Insert like event into ClickHouse
await clickhouse_1.default.query({
query: `
INSERT INTO promote.like_events (user_id, content_id, action)
VALUES (?, ?, ?)
`,
values: [
user.id,
content_id,
action === 'like' ? 1 : 2
]
});
// Queue analytics processing job
await (0, queue_1.addAnalyticsJob)('process_likes', {
user_id: user.id,
content_id,
action,
timestamp: new Date().toISOString()
});
// Update like count in Redis cache
const redis = await (0, redis_1.getRedisClient)();
const likeKey = `likes:${content_id}`;
if (action === 'like') {
await redis.incr(likeKey);
}
else {
await redis.decr(likeKey);
}
return c.json({ message: `${action} tracked successfully` });
}
catch (error) {
console.error('Like tracking error:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// Track a follow event
analyticsRouter.post('/follow', async (c) => {
try {
const { followed_id, action } = await c.req.json();
const user = c.get('user');
if (!followed_id || !action) {
return c.json({ error: 'Followed ID and action are required' }, 400);
}
if (action !== 'follow' && action !== 'unfollow') {
return c.json({ error: 'Action must be either "follow" or "unfollow"' }, 400);
}
// Insert follower event into ClickHouse
await clickhouse_1.default.query({
query: `
INSERT INTO promote.follower_events (follower_id, followed_id, action)
VALUES (?, ?, ?)
`,
values: [
user.id,
followed_id,
action === 'follow' ? 1 : 2
]
});
// Queue analytics processing job
await (0, queue_1.addAnalyticsJob)('process_followers', {
follower_id: user.id,
followed_id,
action,
timestamp: new Date().toISOString()
});
// Update follower count in Redis cache
const redis = await (0, redis_1.getRedisClient)();
const followerKey = `followers:${followed_id}`;
if (action === 'follow') {
await redis.incr(followerKey);
}
else {
await redis.decr(followerKey);
}
return c.json({ message: `${action} tracked successfully` });
}
catch (error) {
console.error('Follow tracking error:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// Get analytics for a content
analyticsRouter.get('/content/:id', async (c) => {
try {
const contentId = c.req.param('id');
// Get counts from Redis cache
const redis = await (0, redis_1.getRedisClient)();
const [views, likes] = await Promise.all([
redis.get(`views:${contentId}`),
redis.get(`likes:${contentId}`)
]);
return c.json({
content_id: contentId,
views: parseInt(views || '0'),
likes: parseInt(likes || '0')
});
}
catch (error) {
console.error('Content analytics error:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// Get analytics for a user
analyticsRouter.get('/user/:id', async (c) => {
try {
const userId = c.req.param('id');
// Get follower count from Redis cache
const redis = await (0, redis_1.getRedisClient)();
const followers = await redis.get(`followers:${userId}`);
// Get content view and like counts from ClickHouse
const viewsResult = await clickhouse_1.default.query({
query: `
SELECT content_id, COUNT(*) as view_count
FROM promote.view_events
WHERE user_id = ?
GROUP BY content_id
`,
values: [userId]
});
const likesResult = await clickhouse_1.default.query({
query: `
SELECT content_id, SUM(CASE WHEN action = 1 THEN 1 ELSE -1 END) as like_count
FROM promote.like_events
WHERE user_id = ?
GROUP BY content_id
`,
values: [userId]
});
// Extract data from results
const viewsData = 'rows' in viewsResult ? viewsResult.rows : [];
const likesData = 'rows' in likesResult ? likesResult.rows : [];
return c.json({
user_id: userId,
followers: parseInt(followers || '0'),
content_analytics: {
views: viewsData,
likes: likesData
}
});
}
catch (error) {
console.error('User analytics error:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 社群分析相关路由
// 获取项目的顶级影响者
analyticsRouter.get('/project/:id/top-influencers', async (c) => {
try {
const projectId = c.req.param('id');
// 从ClickHouse查询项目的顶级影响者
const result = await clickhouse_1.default.query({
query: `
SELECT
influencer_id,
SUM(metric_value) AS total_views
FROM events
WHERE
project_id = ? AND
event_type = 'post_view_change'
GROUP BY influencer_id
ORDER BY total_views DESC
LIMIT 10
`,
values: [projectId]
});
// 提取数据
const influencerData = 'rows' in result ? result.rows : [];
// 如果有数据从Supabase获取影响者详细信息
if (influencerData.length > 0) {
const influencerIds = influencerData.map((item) => item.influencer_id);
const { data: influencerDetails, error } = await supabase_1.default
.from('influencers')
.select('influencer_id, name, platform, followers_count, video_count')
.in('influencer_id', influencerIds);
if (error) {
console.error('Error fetching influencer details:', error);
return c.json({ error: 'Error fetching influencer details' }, 500);
}
// 合并数据
const enrichedData = influencerData.map((item) => {
const details = influencerDetails?.find((detail) => detail.influencer_id === item.influencer_id) || {};
return {
...item,
...details
};
});
return c.json(enrichedData);
}
return c.json(influencerData);
}
catch (error) {
console.error('Error fetching top influencers:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取影响者的粉丝变化趋势过去6个月
analyticsRouter.get('/influencer/:id/follower-trend', async (c) => {
try {
const influencerId = c.req.param('id');
// 从ClickHouse查询影响者的粉丝变化趋势
const result = await clickhouse_1.default.query({
query: `
SELECT
toStartOfMonth(timestamp) AS month,
SUM(metric_value) AS follower_change
FROM events
WHERE
influencer_id = ? AND
event_type = 'follower_change' AND
timestamp >= subtractMonths(now(), 6)
GROUP BY month
ORDER BY month ASC
`,
values: [influencerId]
});
// 提取数据
const trendData = 'rows' in result ? result.rows : [];
return c.json({
influencer_id: influencerId,
follower_trend: trendData
});
}
catch (error) {
console.error('Error fetching follower trend:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取帖子的点赞变化过去30天
analyticsRouter.get('/post/:id/like-trend', async (c) => {
try {
const postId = c.req.param('id');
// 从ClickHouse查询帖子的点赞变化
const result = await clickhouse_1.default.query({
query: `
SELECT
toDate(timestamp) AS day,
SUM(metric_value) AS like_change
FROM events
WHERE
post_id = ? AND
event_type = 'post_like_change' AND
timestamp >= subtractDays(now(), 30)
GROUP BY day
ORDER BY day ASC
`,
values: [postId]
});
// 提取数据
const trendData = 'rows' in result ? result.rows : [];
return c.json({
post_id: postId,
like_trend: trendData
});
}
catch (error) {
console.error('Error fetching like trend:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取影响者详细信息
analyticsRouter.get('/influencer/:id/details', async (c) => {
try {
const influencerId = c.req.param('id');
// 从Supabase获取影响者详细信息
const { data, error } = await supabase_1.default
.from('influencers')
.select('influencer_id, name, platform, profile_url, external_id, followers_count, video_count, platform_count, created_at')
.eq('influencer_id', influencerId)
.single();
if (error) {
console.error('Error fetching influencer details:', error);
return c.json({ error: 'Error fetching influencer details' }, 500);
}
if (!data) {
return c.json({ error: 'Influencer not found' }, 404);
}
return c.json(data);
}
catch (error) {
console.error('Error fetching influencer details:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取影响者的帖子列表
analyticsRouter.get('/influencer/:id/posts', async (c) => {
try {
const influencerId = c.req.param('id');
// 从Supabase获取影响者的帖子列表
const { data, error } = await supabase_1.default
.from('posts')
.select('post_id, influencer_id, platform, post_url, title, description, published_at, created_at')
.eq('influencer_id', influencerId)
.order('published_at', { ascending: false });
if (error) {
console.error('Error fetching influencer posts:', error);
return c.json({ error: 'Error fetching influencer posts' }, 500);
}
return c.json(data || []);
}
catch (error) {
console.error('Error fetching influencer posts:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取帖子的评论列表
analyticsRouter.get('/post/:id/comments', async (c) => {
try {
const postId = c.req.param('id');
// 从Supabase获取帖子的评论列表
const { data, error } = await supabase_1.default
.from('comments')
.select('comment_id, post_id, user_id, content, sentiment_score, created_at')
.eq('post_id', postId)
.order('created_at', { ascending: false });
if (error) {
console.error('Error fetching post comments:', error);
return c.json({ error: 'Error fetching post comments' }, 500);
}
return c.json(data || []);
}
catch (error) {
console.error('Error fetching post comments:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取项目的平台分布
analyticsRouter.get('/project/:id/platform-distribution', async (c) => {
try {
const projectId = c.req.param('id');
// 从ClickHouse查询项目的平台分布
const result = await clickhouse_1.default.query({
query: `
SELECT
platform,
COUNT(DISTINCT influencer_id) AS influencer_count
FROM events
WHERE project_id = ?
GROUP BY platform
ORDER BY influencer_count DESC
`,
values: [projectId]
});
// 提取数据
const distributionData = 'rows' in result ? result.rows : [];
return c.json({
project_id: projectId,
platform_distribution: distributionData
});
}
catch (error) {
console.error('Error fetching platform distribution:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取项目的互动类型分布
analyticsRouter.get('/project/:id/interaction-types', async (c) => {
try {
const projectId = c.req.param('id');
// 从ClickHouse查询项目的互动类型分布
const result = await clickhouse_1.default.query({
query: `
SELECT
event_type,
COUNT(*) AS event_count,
SUM(metric_value) AS total_value
FROM events
WHERE
project_id = ? AND
event_type IN ('click', 'comment', 'share')
GROUP BY event_type
ORDER BY event_count DESC
`,
values: [projectId]
});
// 提取数据
const interactionData = 'rows' in result ? result.rows : [];
return c.json({
project_id: projectId,
interaction_types: interactionData
});
}
catch (error) {
console.error('Error fetching interaction types:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
exports.default = analyticsRouter;

View File

@@ -1,140 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const hono_1 = require("hono");
const auth_1 = require("../middlewares/auth");
const supabase_1 = __importDefault(require("../utils/supabase"));
const authRouter = new hono_1.Hono();
// Register a new user
authRouter.post('/register', async (c) => {
try {
const { email, password, name } = await c.req.json();
// Validate input
if (!email || !password || !name) {
return c.json({ error: 'Email, password, and name are required' }, 400);
}
// Register user with Supabase
const { data: authData, error: authError } = await supabase_1.default.auth.signUp({
email,
password,
});
if (authError) {
return c.json({ error: authError.message }, 400);
}
if (!authData.user) {
return c.json({ error: 'Failed to create user' }, 500);
}
// Create user profile in the database
const { error: profileError } = await supabase_1.default
.from('users')
.insert({
id: authData.user.id,
email: authData.user.email,
name,
created_at: new Date().toISOString(),
});
if (profileError) {
// Attempt to clean up the auth user if profile creation fails
await supabase_1.default.auth.admin.deleteUser(authData.user.id);
return c.json({ error: profileError.message }, 500);
}
// Generate JWT token
const token = (0, auth_1.generateToken)(authData.user.id, authData.user.email);
return c.json({
message: 'User registered successfully',
user: {
id: authData.user.id,
email: authData.user.email,
name,
},
token,
}, 201);
}
catch (error) {
console.error('Registration error:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// Login user
authRouter.post('/login', async (c) => {
try {
const { email, password } = await c.req.json();
const { data, error } = await supabase_1.default.auth.signInWithPassword({
email,
password
});
if (error) {
return c.json({ error: error.message }, 400);
}
// 使用与 authMiddleware 一致的方式创建 JWT
const token = (0, auth_1.generateToken)(data.user.id, data.user.email || '');
// 只返回必要的用户信息和令牌
return c.json({
success: true,
token,
user: {
id: data.user.id,
email: data.user.email
}
});
}
catch (error) {
console.error(error);
return c.json({ error: 'Server error' }, 500);
}
});
// Verify token
authRouter.get('/verify', async (c) => {
try {
const token = c.req.header('Authorization')?.split(' ')[1];
if (!token) {
return c.json({ error: 'No token provided' }, 401);
}
const user = await (0, auth_1.verifySupabaseToken)(token);
if (!user) {
return c.json({ error: 'Invalid token' }, 401);
}
return c.json({
message: 'Token is valid',
user: {
id: user.id,
email: user.email,
},
});
}
catch (error) {
console.error('Token verification error:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// Refresh token
authRouter.post('/refresh-token', async (c) => {
try {
const token = c.req.header('Authorization')?.split(' ')[1];
if (!token) {
return c.json({ error: 'No token provided' }, 401);
}
// 验证当前token
const user = await (0, auth_1.verifySupabaseToken)(token);
if (!user) {
return c.json({ error: 'Invalid token' }, 401);
}
// 生成新token
const newToken = (0, auth_1.generateToken)(user.id, user.email || '');
return c.json({
message: 'Token refreshed successfully',
token: newToken,
user: {
id: user.id,
email: user.email,
},
});
}
catch (error) {
console.error('Token refresh error:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
exports.default = authRouter;

View File

@@ -1,12 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const hono_1 = require("hono");
const commentsController_1 = require("../controllers/commentsController");
const auth_1 = require("../middlewares/auth");
const commentsRouter = new hono_1.Hono();
// Public routes
commentsRouter.get('/', commentsController_1.getComments);
// Protected routes
commentsRouter.post('/', auth_1.authMiddleware, commentsController_1.createComment);
commentsRouter.delete('/:comment_id', auth_1.authMiddleware, commentsController_1.deleteComment);
exports.default = commentsRouter;

View File

@@ -1,649 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const hono_1 = require("hono");
const auth_1 = require("../middlewares/auth");
const clickhouse_1 = __importDefault(require("../utils/clickhouse"));
const supabase_1 = __importDefault(require("../utils/supabase"));
const communityRouter = new hono_1.Hono();
// Apply auth middleware to all routes
communityRouter.use('*', auth_1.authMiddleware);
// 创建新项目
communityRouter.post('/projects', async (c) => {
try {
const { name, description, start_date, end_date } = await c.req.json();
const user = c.get('user');
if (!name) {
return c.json({ error: 'Project name is required' }, 400);
}
// 在Supabase中创建项目
const { data, error } = await supabase_1.default
.from('projects')
.insert({
name,
description,
start_date,
end_date,
created_by: user.id
})
.select()
.single();
if (error) {
console.error('Error creating project:', error);
return c.json({ error: 'Failed to create project' }, 500);
}
return c.json({
message: 'Project created successfully',
project: data
}, 201);
}
catch (error) {
console.error('Error creating project:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取项目列表
communityRouter.get('/projects', async (c) => {
try {
const user = c.get('user');
// 从Supabase获取项目列表
const { data, error } = await supabase_1.default
.from('projects')
.select('*')
.eq('created_by', user.id)
.order('created_at', { ascending: false });
if (error) {
console.error('Error fetching projects:', error);
return c.json({ error: 'Failed to fetch projects' }, 500);
}
return c.json(data || []);
}
catch (error) {
console.error('Error fetching projects:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取项目详情
communityRouter.get('/projects/:id', async (c) => {
try {
const projectId = c.req.param('id');
// 从Supabase获取项目详情
const { data, error } = await supabase_1.default
.from('projects')
.select('*')
.eq('id', projectId)
.single();
if (error) {
console.error('Error fetching project:', error);
return c.json({ error: 'Failed to fetch project' }, 500);
}
if (!data) {
return c.json({ error: 'Project not found' }, 404);
}
return c.json(data);
}
catch (error) {
console.error('Error fetching project:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 更新项目
communityRouter.put('/projects/:id', async (c) => {
try {
const projectId = c.req.param('id');
const { name, description, start_date, end_date, status } = await c.req.json();
const user = c.get('user');
// 检查项目是否存在并属于当前用户
const { data: existingProject, error: fetchError } = await supabase_1.default
.from('projects')
.select('*')
.eq('id', projectId)
.eq('created_by', user.id)
.single();
if (fetchError || !existingProject) {
return c.json({ error: 'Project not found or you do not have permission to update it' }, 404);
}
// 更新项目
const { data, error } = await supabase_1.default
.from('projects')
.update({
name,
description,
start_date,
end_date,
status,
updated_at: new Date().toISOString()
})
.eq('id', projectId)
.select()
.single();
if (error) {
console.error('Error updating project:', error);
return c.json({ error: 'Failed to update project' }, 500);
}
return c.json({
message: 'Project updated successfully',
project: data
});
}
catch (error) {
console.error('Error updating project:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 删除项目
communityRouter.delete('/projects/:id', async (c) => {
try {
const projectId = c.req.param('id');
const user = c.get('user');
// 检查项目是否存在并属于当前用户
const { data: existingProject, error: fetchError } = await supabase_1.default
.from('projects')
.select('*')
.eq('id', projectId)
.eq('created_by', user.id)
.single();
if (fetchError || !existingProject) {
return c.json({ error: 'Project not found or you do not have permission to delete it' }, 404);
}
// 删除项目
const { error } = await supabase_1.default
.from('projects')
.delete()
.eq('id', projectId);
if (error) {
console.error('Error deleting project:', error);
return c.json({ error: 'Failed to delete project' }, 500);
}
return c.json({
message: 'Project deleted successfully'
});
}
catch (error) {
console.error('Error deleting project:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 添加影响者到项目
communityRouter.post('/projects/:id/influencers', async (c) => {
try {
const projectId = c.req.param('id');
const { influencer_id, platform, external_id, name, profile_url } = await c.req.json();
const user = c.get('user');
// 检查项目是否存在并属于当前用户
const { data: existingProject, error: fetchError } = await supabase_1.default
.from('projects')
.select('*')
.eq('id', projectId)
.eq('created_by', user.id)
.single();
if (fetchError || !existingProject) {
return c.json({ error: 'Project not found or you do not have permission to update it' }, 404);
}
// 检查影响者是否已存在
let influencerData;
if (influencer_id) {
// 如果提供了影响者ID检查是否存在
const { data, error } = await supabase_1.default
.from('influencers')
.select('*')
.eq('influencer_id', influencer_id)
.single();
if (!error && data) {
influencerData = data;
}
}
else if (external_id && platform) {
// 如果提供了外部ID和平台检查是否存在
const { data, error } = await supabase_1.default
.from('influencers')
.select('*')
.eq('external_id', external_id)
.eq('platform', platform)
.single();
if (!error && data) {
influencerData = data;
}
}
// 如果影响者不存在,创建新的影响者
if (!influencerData) {
if (!name || !platform) {
return c.json({ error: 'Name and platform are required for new influencers' }, 400);
}
const { data, error } = await supabase_1.default
.from('influencers')
.insert({
name,
platform,
external_id,
profile_url
})
.select()
.single();
if (error) {
console.error('Error creating influencer:', error);
return c.json({ error: 'Failed to create influencer' }, 500);
}
influencerData = data;
}
// 将影响者添加到项目
const { data: projectInfluencer, error } = await supabase_1.default
.from('project_influencers')
.insert({
project_id: projectId,
influencer_id: influencerData.influencer_id
})
.select()
.single();
if (error) {
console.error('Error adding influencer to project:', error);
return c.json({ error: 'Failed to add influencer to project' }, 500);
}
return c.json({
message: 'Influencer added to project successfully',
project_influencer: projectInfluencer,
influencer: influencerData
}, 201);
}
catch (error) {
console.error('Error adding influencer to project:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取项目的影响者列表
communityRouter.get('/projects/:id/influencers', async (c) => {
try {
const projectId = c.req.param('id');
// 从Supabase获取项目的影响者列表
const { data, error } = await supabase_1.default
.from('project_influencers')
.select(`
project_id,
influencers (
influencer_id,
name,
platform,
profile_url,
external_id,
followers_count,
video_count
)
`)
.eq('project_id', projectId);
if (error) {
console.error('Error fetching project influencers:', error);
return c.json({ error: 'Failed to fetch project influencers' }, 500);
}
// 格式化数据
const influencers = data?.map(item => item.influencers) || [];
return c.json(influencers);
}
catch (error) {
console.error('Error fetching project influencers:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 从项目中移除影响者
communityRouter.delete('/projects/:projectId/influencers/:influencerId', async (c) => {
try {
const projectId = c.req.param('projectId');
const influencerId = c.req.param('influencerId');
const user = c.get('user');
// 检查项目是否存在并属于当前用户
const { data: existingProject, error: fetchError } = await supabase_1.default
.from('projects')
.select('*')
.eq('id', projectId)
.eq('created_by', user.id)
.single();
if (fetchError || !existingProject) {
return c.json({ error: 'Project not found or you do not have permission to update it' }, 404);
}
// 从项目中移除影响者
const { error } = await supabase_1.default
.from('project_influencers')
.delete()
.eq('project_id', projectId)
.eq('influencer_id', influencerId);
if (error) {
console.error('Error removing influencer from project:', error);
return c.json({ error: 'Failed to remove influencer from project' }, 500);
}
return c.json({
message: 'Influencer removed from project successfully'
});
}
catch (error) {
console.error('Error removing influencer from project:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 添加事件数据
communityRouter.post('/events', async (c) => {
try {
const { project_id, influencer_id, post_id, platform, event_type, metric_value, event_metadata } = await c.req.json();
if (!project_id || !influencer_id || !platform || !event_type || metric_value === undefined) {
return c.json({
error: 'Project ID, influencer ID, platform, event type, and metric value are required'
}, 400);
}
// 验证事件类型
const validEventTypes = [
'follower_change',
'post_like_change',
'post_view_change',
'click',
'comment',
'share'
];
if (!validEventTypes.includes(event_type)) {
return c.json({
error: `Invalid event type. Must be one of: ${validEventTypes.join(', ')}`
}, 400);
}
// 验证平台
const validPlatforms = ['youtube', 'instagram', 'tiktok', 'twitter', 'facebook'];
if (!validPlatforms.includes(platform)) {
return c.json({
error: `Invalid platform. Must be one of: ${validPlatforms.join(', ')}`
}, 400);
}
// 将事件数据插入ClickHouse
await clickhouse_1.default.query({
query: `
INSERT INTO events (
project_id,
influencer_id,
post_id,
platform,
event_type,
metric_value,
event_metadata
) VALUES (?, ?, ?, ?, ?, ?, ?)
`,
values: [
project_id,
influencer_id,
post_id || null,
platform,
event_type,
metric_value,
event_metadata ? JSON.stringify(event_metadata) : '{}'
]
});
return c.json({
message: 'Event data added successfully'
}, 201);
}
catch (error) {
console.error('Error adding event data:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 批量添加事件数据
communityRouter.post('/events/batch', async (c) => {
try {
const { events } = await c.req.json();
if (!Array.isArray(events) || events.length === 0) {
return c.json({ error: 'Events array is required and must not be empty' }, 400);
}
// 验证事件类型和平台
const validEventTypes = [
'follower_change',
'post_like_change',
'post_view_change',
'click',
'comment',
'share'
];
const validPlatforms = ['youtube', 'instagram', 'tiktok', 'twitter', 'facebook'];
// 验证每个事件
for (const event of events) {
const { project_id, influencer_id, platform, event_type, metric_value } = event;
if (!project_id || !influencer_id || !platform || !event_type || metric_value === undefined) {
return c.json({
error: 'Project ID, influencer ID, platform, event type, and metric value are required for all events'
}, 400);
}
if (!validEventTypes.includes(event_type)) {
return c.json({
error: `Invalid event type: ${event_type}. Must be one of: ${validEventTypes.join(', ')}`
}, 400);
}
if (!validPlatforms.includes(platform)) {
return c.json({
error: `Invalid platform: ${platform}. Must be one of: ${validPlatforms.join(', ')}`
}, 400);
}
}
// 准备批量插入数据
const values = events.map(event => `(
'${event.project_id}',
'${event.influencer_id}',
${event.post_id ? `'${event.post_id}'` : 'NULL'},
'${event.platform}',
'${event.event_type}',
${event.metric_value},
'${event.event_metadata ? JSON.stringify(event.event_metadata) : '{}'}'
)`).join(',');
// 批量插入事件数据
await clickhouse_1.default.query({
query: `
INSERT INTO events (
project_id,
influencer_id,
post_id,
platform,
event_type,
metric_value,
event_metadata
) VALUES ${values}
`
});
return c.json({
message: `${events.length} events added successfully`
}, 201);
}
catch (error) {
console.error('Error adding batch event data:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 添加帖子
communityRouter.post('/posts', async (c) => {
try {
const { influencer_id, platform, post_url, title, description, published_at } = await c.req.json();
if (!influencer_id || !platform || !post_url) {
return c.json({
error: 'Influencer ID, platform, and post URL are required'
}, 400);
}
// 验证平台
const validPlatforms = ['youtube', 'instagram', 'tiktok', 'twitter', 'facebook'];
if (!validPlatforms.includes(platform)) {
return c.json({
error: `Invalid platform. Must be one of: ${validPlatforms.join(', ')}`
}, 400);
}
// 检查帖子是否已存在
const { data: existingPost, error: checkError } = await supabase_1.default
.from('posts')
.select('*')
.eq('post_url', post_url)
.single();
if (!checkError && existingPost) {
return c.json({
error: 'Post with this URL already exists',
post: existingPost
}, 409);
}
// 创建新帖子
const { data, error } = await supabase_1.default
.from('posts')
.insert({
influencer_id,
platform,
post_url,
title,
description,
published_at: published_at || new Date().toISOString()
})
.select()
.single();
if (error) {
console.error('Error creating post:', error);
return c.json({ error: 'Failed to create post' }, 500);
}
return c.json({
message: 'Post created successfully',
post: data
}, 201);
}
catch (error) {
console.error('Error creating post:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 添加评论
communityRouter.post('/comments', async (c) => {
try {
const { post_id, user_id, content, sentiment_score } = await c.req.json();
if (!post_id || !content) {
return c.json({
error: 'Post ID and content are required'
}, 400);
}
// 创建新评论
const { data, error } = await supabase_1.default
.from('comments')
.insert({
post_id,
user_id: user_id || c.get('user').id,
content,
sentiment_score: sentiment_score || 0
})
.select()
.single();
if (error) {
console.error('Error creating comment:', error);
return c.json({ error: 'Failed to create comment' }, 500);
}
return c.json({
message: 'Comment created successfully',
comment: data
}, 201);
}
catch (error) {
console.error('Error creating comment:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取项目的事件统计
communityRouter.get('/projects/:id/event-stats', async (c) => {
try {
const projectId = c.req.param('id');
// 从ClickHouse查询项目的事件统计
const result = await clickhouse_1.default.query({
query: `
SELECT
event_type,
COUNT(*) AS event_count,
SUM(metric_value) AS total_value
FROM events
WHERE project_id = ?
GROUP BY event_type
ORDER BY event_count DESC
`,
values: [projectId]
});
// 提取数据
const statsData = 'rows' in result ? result.rows : [];
return c.json({
project_id: projectId,
event_stats: statsData
});
}
catch (error) {
console.error('Error fetching event stats:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取项目的时间趋势
communityRouter.get('/projects/:id/time-trend', async (c) => {
try {
const projectId = c.req.param('id');
const { event_type, interval = 'day', days = '30' } = c.req.query();
if (!event_type) {
return c.json({ error: 'Event type is required' }, 400);
}
// 验证事件类型
const validEventTypes = [
'follower_change',
'post_like_change',
'post_view_change',
'click',
'comment',
'share'
];
if (!validEventTypes.includes(event_type)) {
return c.json({
error: `Invalid event type. Must be one of: ${validEventTypes.join(', ')}`
}, 400);
}
// 验证时间间隔
const validIntervals = ['hour', 'day', 'week', 'month'];
if (!validIntervals.includes(interval)) {
return c.json({
error: `Invalid interval. Must be one of: ${validIntervals.join(', ')}`
}, 400);
}
// 构建时间间隔函数
let timeFunction;
switch (interval) {
case 'hour':
timeFunction = 'toStartOfHour';
break;
case 'day':
timeFunction = 'toDate';
break;
case 'week':
timeFunction = 'toStartOfWeek';
break;
case 'month':
timeFunction = 'toStartOfMonth';
break;
}
// 从ClickHouse查询项目的时间趋势
const result = await clickhouse_1.default.query({
query: `
SELECT
${timeFunction}(timestamp) AS time_period,
SUM(metric_value) AS value
FROM events
WHERE
project_id = ? AND
event_type = ? AND
timestamp >= subtractDays(now(), ?)
GROUP BY time_period
ORDER BY time_period ASC
`,
values: [projectId, event_type, parseInt(days)]
});
// 提取数据
const trendData = 'rows' in result ? result.rows : [];
return c.json({
project_id: projectId,
event_type,
interval,
days: parseInt(days),
trend: trendData
});
}
catch (error) {
console.error('Error fetching time trend:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
exports.default = communityRouter;

View File

@@ -1,10 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const hono_1 = require("hono");
const influencersController_1 = require("../controllers/influencersController");
const influencersRouter = new hono_1.Hono();
// Public routes
influencersRouter.get('/', influencersController_1.getInfluencers);
influencersRouter.get('/stats', influencersController_1.getInfluencerStats);
influencersRouter.get('/:influencer_id', influencersController_1.getInfluencerById);
exports.default = influencersRouter;

View File

@@ -1,584 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const hono_1 = require("hono");
const auth_1 = require("../middlewares/auth");
const supabase_1 = __importDefault(require("../utils/supabase"));
const clickhouse_1 = __importDefault(require("../utils/clickhouse"));
const redis_1 = require("../utils/redis");
const postsRouter = new hono_1.Hono();
// Apply auth middleware to most routes
postsRouter.use('*', auth_1.authMiddleware);
// 创建新帖子
postsRouter.post('/', async (c) => {
try {
const { influencer_id, platform, post_url, title, description, published_at } = await c.req.json();
if (!influencer_id || !platform || !post_url) {
return c.json({
error: 'influencer_id, platform, and post_url are required'
}, 400);
}
// 验证平台
const validPlatforms = ['youtube', 'instagram', 'tiktok', 'twitter', 'facebook'];
if (!validPlatforms.includes(platform)) {
return c.json({
error: `Invalid platform. Must be one of: ${validPlatforms.join(', ')}`
}, 400);
}
// 检查帖子URL是否已存在
const { data: existingPost, error: checkError } = await supabase_1.default
.from('posts')
.select('*')
.eq('post_url', post_url)
.single();
if (!checkError && existingPost) {
return c.json({
error: 'Post with this URL already exists',
post: existingPost
}, 409);
}
// 创建新帖子
const { data: post, error } = await supabase_1.default
.from('posts')
.insert({
influencer_id,
platform,
post_url,
title,
description,
published_at: published_at || new Date().toISOString()
})
.select()
.single();
if (error) {
console.error('Error creating post:', error);
return c.json({ error: 'Failed to create post' }, 500);
}
return c.json({
message: 'Post created successfully',
post
}, 201);
}
catch (error) {
console.error('Error creating post:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取帖子列表
postsRouter.get('/', async (c) => {
try {
const { influencer_id, platform, limit = '20', offset = '0', sort = 'published_at', order = 'desc' } = c.req.query();
// 构建查询
let query = supabase_1.default.from('posts').select(`
*,
influencer:influencers(name, platform, profile_url, followers_count)
`);
// 添加过滤条件
if (influencer_id) {
query = query.eq('influencer_id', influencer_id);
}
if (platform) {
query = query.eq('platform', platform);
}
// 添加排序和分页
query = query.order(sort, { ascending: order === 'asc' });
query = query.range(parseInt(offset), parseInt(offset) + parseInt(limit) - 1);
// 执行查询
const { data, error, count } = await query;
if (error) {
console.error('Error fetching posts:', error);
return c.json({ error: 'Failed to fetch posts' }, 500);
}
// 获取帖子的统计数据
if (data && data.length > 0) {
const postIds = data.map(post => post.post_id);
// 尝试从缓存获取数据
const redis = await (0, redis_1.getRedisClient)();
const cachedStats = await Promise.all(postIds.map(async (postId) => {
const [views, likes] = await Promise.all([
redis.get(`post:views:${postId}`),
redis.get(`post:likes:${postId}`)
]);
return {
post_id: postId,
views: views ? parseInt(views) : null,
likes: likes ? parseInt(likes) : null
};
}));
// 找出缓存中没有的帖子ID
const missingIds = postIds.filter(id => {
const stat = cachedStats.find(s => s.post_id === id);
return stat?.views === null || stat?.likes === null;
});
// 如果有缺失的统计数据从ClickHouse获取
if (missingIds.length > 0) {
try {
// 查询帖子的观看数
const viewsResult = await clickhouse_1.default.query({
query: `
SELECT
post_id,
SUM(metric_value) AS views
FROM events
WHERE
post_id IN (${missingIds.map(id => `'${id}'`).join(',')}) AND
event_type = 'post_view_change'
GROUP BY post_id
`
});
// 查询帖子的点赞数
const likesResult = await clickhouse_1.default.query({
query: `
SELECT
post_id,
SUM(metric_value) AS likes
FROM events
WHERE
post_id IN (${missingIds.map(id => `'${id}'`).join(',')}) AND
event_type = 'post_like_change'
GROUP BY post_id
`
});
// 处理结果
const viewsData = 'rows' in viewsResult ? viewsResult.rows : [];
const likesData = 'rows' in likesResult ? likesResult.rows : [];
// 更新缓存并填充统计数据
for (const viewStat of viewsData) {
if (viewStat && typeof viewStat === 'object' && 'post_id' in viewStat && 'views' in viewStat) {
// 更新缓存
await redis.set(`post:views:${viewStat.post_id}`, String(viewStat.views));
// 更新缓存统计数据
const cacheStat = cachedStats.find(s => s.post_id === viewStat.post_id);
if (cacheStat) {
cacheStat.views = Number(viewStat.views);
}
}
}
for (const likeStat of likesData) {
if (likeStat && typeof likeStat === 'object' && 'post_id' in likeStat && 'likes' in likeStat) {
// 更新缓存
await redis.set(`post:likes:${likeStat.post_id}`, String(likeStat.likes));
// 更新缓存统计数据
const cacheStat = cachedStats.find(s => s.post_id === likeStat.post_id);
if (cacheStat) {
cacheStat.likes = Number(likeStat.likes);
}
}
}
}
catch (chError) {
console.error('Error fetching stats from ClickHouse:', chError);
}
}
// 合并统计数据到帖子数据
data.forEach(post => {
const stats = cachedStats.find(s => s.post_id === post.post_id);
post.stats = {
views: stats?.views || 0,
likes: stats?.likes || 0
};
});
}
return c.json({
posts: data || [],
total: count || 0,
limit: parseInt(limit),
offset: parseInt(offset)
});
}
catch (error) {
console.error('Error fetching posts:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取单个帖子详情
postsRouter.get('/:id', async (c) => {
try {
const postId = c.req.param('id');
// 获取帖子详情
const { data: post, error } = await supabase_1.default
.from('posts')
.select(`
*,
influencer:influencers(name, platform, profile_url, followers_count)
`)
.eq('post_id', postId)
.single();
if (error) {
console.error('Error fetching post:', error);
return c.json({ error: 'Failed to fetch post' }, 500);
}
if (!post) {
return c.json({ error: 'Post not found' }, 404);
}
// 获取帖子统计数据
try {
// 先尝试从Redis缓存获取
const redis = await (0, redis_1.getRedisClient)();
const [cachedViews, cachedLikes] = await Promise.all([
redis.get(`post:views:${postId}`),
redis.get(`post:likes:${postId}`)
]);
// 如果缓存中有数据,直接使用
if (cachedViews !== null && cachedLikes !== null) {
post.stats = {
views: parseInt(cachedViews),
likes: parseInt(cachedLikes)
};
}
else {
// 如果缓存中没有从ClickHouse获取
// 查询帖子的观看数
const viewsResult = await clickhouse_1.default.query({
query: `
SELECT SUM(metric_value) AS views
FROM events
WHERE
post_id = ? AND
event_type = 'post_view_change'
`,
values: [postId]
});
// 查询帖子的点赞数
const likesResult = await clickhouse_1.default.query({
query: `
SELECT SUM(metric_value) AS likes
FROM events
WHERE
post_id = ? AND
event_type = 'post_like_change'
`,
values: [postId]
});
// 处理结果
let viewsData = 0;
if ('rows' in viewsResult && viewsResult.rows.length > 0 && viewsResult.rows[0] && typeof viewsResult.rows[0] === 'object' && 'views' in viewsResult.rows[0]) {
viewsData = Number(viewsResult.rows[0].views) || 0;
}
let likesData = 0;
if ('rows' in likesResult && likesResult.rows.length > 0 && likesResult.rows[0] && typeof likesResult.rows[0] === 'object' && 'likes' in likesResult.rows[0]) {
likesData = Number(likesResult.rows[0].likes) || 0;
}
// 更新缓存
await redis.set(`post:views:${postId}`, String(viewsData));
await redis.set(`post:likes:${postId}`, String(likesData));
// 添加统计数据
post.stats = {
views: viewsData,
likes: likesData
};
}
// 获取互动时间线
const timelineResult = await clickhouse_1.default.query({
query: `
SELECT
toDate(timestamp) as date,
event_type,
SUM(metric_value) as value
FROM events
WHERE
post_id = ? AND
event_type IN ('post_view_change', 'post_like_change')
GROUP BY date, event_type
ORDER BY date ASC
`,
values: [postId]
});
const timelineData = 'rows' in timelineResult ? timelineResult.rows : [];
// 添加时间线数据
post.timeline = timelineData;
// 获取评论数量
const { count } = await supabase_1.default
.from('comments')
.select('*', { count: 'exact', head: true })
.eq('post_id', postId);
post.comment_count = count || 0;
}
catch (statsError) {
console.error('Error fetching post stats:', statsError);
// 继续返回帖子数据,但没有统计信息
post.stats = { views: 0, likes: 0 };
post.timeline = [];
post.comment_count = 0;
}
return c.json(post);
}
catch (error) {
console.error('Error fetching post:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 更新帖子
postsRouter.put('/:id', async (c) => {
try {
const postId = c.req.param('id');
const user = c.get('user');
const { title, description } = await c.req.json();
// 先检查帖子是否存在
const { data: existingPost, error: fetchError } = await supabase_1.default
.from('posts')
.select('*')
.eq('post_id', postId)
.single();
if (fetchError || !existingPost) {
return c.json({ error: 'Post not found' }, 404);
}
// 更新帖子
const { data: updatedPost, error } = await supabase_1.default
.from('posts')
.update({
title,
description,
updated_at: new Date().toISOString()
})
.eq('post_id', postId)
.select()
.single();
if (error) {
console.error('Error updating post:', error);
return c.json({ error: 'Failed to update post' }, 500);
}
return c.json({
message: 'Post updated successfully',
post: updatedPost
});
}
catch (error) {
console.error('Error updating post:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 删除帖子
postsRouter.delete('/:id', async (c) => {
try {
const postId = c.req.param('id');
const user = c.get('user');
// 删除帖子
const { error } = await supabase_1.default
.from('posts')
.delete()
.eq('post_id', postId);
if (error) {
console.error('Error deleting post:', error);
return c.json({ error: 'Failed to delete post' }, 500);
}
// 清除缓存
try {
const redis = await (0, redis_1.getRedisClient)();
await Promise.all([
redis.del(`post:views:${postId}`),
redis.del(`post:likes:${postId}`)
]);
}
catch (cacheError) {
console.error('Error clearing cache:', cacheError);
}
return c.json({
message: 'Post deleted successfully'
});
}
catch (error) {
console.error('Error deleting post:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取帖子的评论
postsRouter.get('/:id/comments', async (c) => {
try {
const postId = c.req.param('id');
const { limit = '20', offset = '0' } = c.req.query();
// 获取评论
const { data: comments, error, count } = await supabase_1.default
.from('comments')
.select('*', { count: 'exact' })
.eq('post_id', postId)
.order('created_at', { ascending: false })
.range(parseInt(offset), parseInt(offset) + parseInt(limit) - 1);
if (error) {
console.error('Error fetching comments:', error);
return c.json({ error: 'Failed to fetch comments' }, 500);
}
// 如果有评论,获取用户信息
if (comments && comments.length > 0) {
const userIds = [...new Set(comments.map(comment => comment.user_id))];
// 获取用户信息
const { data: userProfiles, error: userError } = await supabase_1.default
.from('user_profiles')
.select('id, full_name, avatar_url')
.in('id', userIds);
if (!userError && userProfiles) {
// 将用户信息添加到评论中
comments.forEach(comment => {
const userProfile = userProfiles.find(profile => profile.id === comment.user_id);
comment.user_profile = userProfile || null;
});
}
else {
console.error('Error fetching user profiles:', userError);
}
}
return c.json({
comments: comments || [],
total: count || 0,
limit: parseInt(limit),
offset: parseInt(offset)
});
}
catch (error) {
console.error('Error fetching comments:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 添加评论到帖子
postsRouter.post('/:id/comments', async (c) => {
try {
const postId = c.req.param('id');
const user = c.get('user');
const { content, sentiment_score } = await c.req.json();
if (!content) {
return c.json({ error: 'Comment content is required' }, 400);
}
// 创建评论
const { data: comment, error } = await supabase_1.default
.from('comments')
.insert({
post_id: postId,
user_id: user.id,
content,
sentiment_score: sentiment_score || 0
})
.select()
.single();
if (error) {
console.error('Error creating comment:', error);
return c.json({ error: 'Failed to create comment' }, 500);
}
// 尝试记录评论事件到ClickHouse
try {
// 获取帖子信息
const { data: post } = await supabase_1.default
.from('posts')
.select('influencer_id, platform')
.eq('post_id', postId)
.single();
if (post) {
await clickhouse_1.default.query({
query: `
INSERT INTO events (
influencer_id,
post_id,
platform,
event_type,
metric_value,
event_metadata
) VALUES (?, ?, ?, 'comment', ?, ?)
`,
values: [
post.influencer_id,
postId,
post.platform,
1,
JSON.stringify({
comment_id: comment.comment_id,
user_id: user.id,
sentiment_score: sentiment_score || 0
})
]
});
}
}
catch (eventError) {
console.error('Error recording comment event:', eventError);
// 不影响主流程,继续返回评论数据
}
return c.json({
message: 'Comment added successfully',
comment
}, 201);
}
catch (error) {
console.error('Error adding comment:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 更新评论
postsRouter.put('/comments/:id', async (c) => {
try {
const commentId = c.req.param('id');
const user = c.get('user');
const { content, sentiment_score } = await c.req.json();
// 先检查评论是否存在且属于当前用户
const { data: existingComment, error: fetchError } = await supabase_1.default
.from('comments')
.select('*')
.eq('comment_id', commentId)
.eq('user_id', user.id)
.single();
if (fetchError || !existingComment) {
return c.json({
error: 'Comment not found or you do not have permission to update it'
}, 404);
}
// 更新评论
const { data: updatedComment, error } = await supabase_1.default
.from('comments')
.update({
content,
sentiment_score: sentiment_score !== undefined ? sentiment_score : existingComment.sentiment_score,
updated_at: new Date().toISOString()
})
.eq('comment_id', commentId)
.select()
.single();
if (error) {
console.error('Error updating comment:', error);
return c.json({ error: 'Failed to update comment' }, 500);
}
return c.json({
message: 'Comment updated successfully',
comment: updatedComment
});
}
catch (error) {
console.error('Error updating comment:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 删除评论
postsRouter.delete('/comments/:id', async (c) => {
try {
const commentId = c.req.param('id');
const user = c.get('user');
// 先检查评论是否存在且属于当前用户
const { data: existingComment, error: fetchError } = await supabase_1.default
.from('comments')
.select('*')
.eq('comment_id', commentId)
.eq('user_id', user.id)
.single();
if (fetchError || !existingComment) {
return c.json({
error: 'Comment not found or you do not have permission to delete it'
}, 404);
}
// 删除评论
const { error } = await supabase_1.default
.from('comments')
.delete()
.eq('comment_id', commentId);
if (error) {
console.error('Error deleting comment:', error);
return c.json({ error: 'Failed to delete comment' }, 500);
}
return c.json({
message: 'Comment deleted successfully'
});
}
catch (error) {
console.error('Error deleting comment:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
exports.default = postsRouter;

View File

@@ -1,395 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const hono_1 = require("hono");
const auth_1 = require("../middlewares/auth");
const supabase_1 = __importDefault(require("../utils/supabase"));
const clickhouse_1 = __importDefault(require("../utils/clickhouse"));
const projectCommentsRouter = new hono_1.Hono();
// Apply auth middleware to all routes
projectCommentsRouter.use('*', auth_1.authMiddleware);
// 获取项目的评论列表
projectCommentsRouter.get('/projects/:id/comments', async (c) => {
try {
const projectId = c.req.param('id');
const { limit = '20', offset = '0', parent_id = null } = c.req.query();
// 检查项目是否存在
const { data: project, error: projectError } = await supabase_1.default
.from('projects')
.select('id, name')
.eq('id', projectId)
.single();
if (projectError) {
console.error('Error fetching project:', projectError);
return c.json({ error: 'Project not found' }, 404);
}
// 构建评论查询
let commentsQuery = supabase_1.default
.from('project_comments')
.select(`
comment_id,
project_id,
user_id,
content,
sentiment_score,
status,
is_pinned,
parent_id,
created_at,
updated_at,
user:user_id(id, email)
`, { count: 'exact' });
// 过滤条件
commentsQuery = commentsQuery.eq('project_id', projectId);
// 如果指定了父评论ID则获取子评论
if (parent_id) {
commentsQuery = commentsQuery.eq('parent_id', parent_id);
}
else {
// 否则获取顶级评论(没有父评论的评论)
commentsQuery = commentsQuery.is('parent_id', null);
}
// 排序和分页
const isPinned = parent_id ? false : true; // 只有顶级评论才考虑置顶
if (isPinned) {
commentsQuery = commentsQuery.order('is_pinned', { ascending: false });
}
commentsQuery = commentsQuery.order('created_at', { ascending: false });
commentsQuery = commentsQuery.range(parseInt(offset), parseInt(offset) + parseInt(limit) - 1);
// 执行查询
const { data: comments, error: commentsError, count } = await commentsQuery;
if (commentsError) {
console.error('Error fetching project comments:', commentsError);
return c.json({ error: 'Failed to fetch project comments' }, 500);
}
// 获取每个顶级评论的回复数量
if (comments && !parent_id) {
const commentIds = comments.map(comment => comment.comment_id);
if (commentIds.length > 0) {
// 手动构建SQL查询来计算每个父评论的回复数量
const { data: replyCounts, error: replyCountError } = await supabase_1.default
.rpc('get_reply_counts_for_comments', { parent_ids: commentIds });
if (!replyCountError && replyCounts) {
// 将回复数量添加到评论中
for (const comment of comments) {
const replyCountItem = replyCounts.find((r) => r.parent_id === comment.comment_id);
comment.reply_count = replyCountItem ? replyCountItem.count : 0;
}
}
}
}
return c.json({
project,
comments: comments || [],
total: count || 0,
limit: parseInt(limit),
offset: parseInt(offset)
});
}
catch (error) {
console.error('Error fetching project comments:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 添加评论到项目
projectCommentsRouter.post('/projects/:id/comments', async (c) => {
try {
const projectId = c.req.param('id');
const user = c.get('user');
const { content, sentiment_score = 0, parent_id = null } = await c.req.json();
if (!content) {
return c.json({ error: 'Comment content is required' }, 400);
}
// 检查项目是否存在
const { data: project, error: projectError } = await supabase_1.default
.from('projects')
.select('id')
.eq('id', projectId)
.single();
if (projectError) {
console.error('Error fetching project:', projectError);
return c.json({ error: 'Project not found' }, 404);
}
// 如果指定了父评论ID检查父评论是否存在
if (parent_id) {
const { data: parentComment, error: parentError } = await supabase_1.default
.from('project_comments')
.select('comment_id')
.eq('comment_id', parent_id)
.eq('project_id', projectId)
.single();
if (parentError || !parentComment) {
return c.json({ error: 'Parent comment not found' }, 404);
}
}
// 创建评论
const { data: comment, error: commentError } = await supabase_1.default
.from('project_comments')
.insert({
project_id: projectId,
user_id: user.id,
content,
sentiment_score,
parent_id
})
.select()
.single();
if (commentError) {
console.error('Error creating project comment:', commentError);
return c.json({ error: 'Failed to create comment' }, 500);
}
// 记录评论事件到ClickHouse
try {
await clickhouse_1.default.query({
query: `
INSERT INTO events (
project_id,
event_type,
metric_value,
event_metadata
) VALUES (?, 'project_comment', ?, ?)
`,
values: [
projectId,
1,
JSON.stringify({
comment_id: comment.comment_id,
user_id: user.id,
parent_id: parent_id || null,
content: content.substring(0, 100), // 只存储部分内容以减小数据量
sentiment_score: sentiment_score
})
]
});
}
catch (chError) {
console.error('Error recording project comment event:', chError);
// 继续执行,不中断主流程
}
return c.json({
message: 'Comment added successfully',
comment
}, 201);
}
catch (error) {
console.error('Error adding project comment:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 更新项目评论
projectCommentsRouter.put('/comments/:id', async (c) => {
try {
const commentId = c.req.param('id');
const user = c.get('user');
const { content, sentiment_score, is_pinned } = await c.req.json();
// 检查评论是否存在且属于当前用户或用户是项目拥有者
const { data: comment, error: fetchError } = await supabase_1.default
.from('project_comments')
.select(`
comment_id,
project_id,
user_id,
projects!inner(created_by)
`)
.eq('comment_id', commentId)
.single();
if (fetchError || !comment) {
return c.json({ error: 'Comment not found' }, 404);
}
// 确保我们能够安全地访问projects中的created_by字段
const projectOwner = comment.projects &&
Array.isArray(comment.projects) &&
comment.projects.length > 0 ?
comment.projects[0].created_by : null;
// 检查用户是否有权限更新评论
const isCommentOwner = comment.user_id === user.id;
const isProjectOwner = projectOwner === user.id;
if (!isCommentOwner && !isProjectOwner) {
return c.json({
error: 'You do not have permission to update this comment'
}, 403);
}
// 准备更新数据
const updateData = {};
// 评论创建者可以更新内容和情感分数
if (isCommentOwner) {
if (content !== undefined) {
updateData.content = content;
}
if (sentiment_score !== undefined) {
updateData.sentiment_score = sentiment_score;
}
}
// 项目所有者可以更新状态和置顶
if (isProjectOwner) {
if (is_pinned !== undefined) {
updateData.is_pinned = is_pinned;
}
}
// 更新时间
updateData.updated_at = new Date().toISOString();
// 如果没有内容要更新,返回错误
if (Object.keys(updateData).length === 1) { // 只有updated_at
return c.json({ error: 'No valid fields to update' }, 400);
}
// 更新评论
const { data: updatedComment, error } = await supabase_1.default
.from('project_comments')
.update(updateData)
.eq('comment_id', commentId)
.select()
.single();
if (error) {
console.error('Error updating project comment:', error);
return c.json({ error: 'Failed to update comment' }, 500);
}
return c.json({
message: 'Comment updated successfully',
comment: updatedComment
});
}
catch (error) {
console.error('Error updating project comment:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 删除项目评论
projectCommentsRouter.delete('/comments/:id', async (c) => {
try {
const commentId = c.req.param('id');
const user = c.get('user');
// 检查评论是否存在且属于当前用户或用户是项目拥有者
const { data: comment, error: fetchError } = await supabase_1.default
.from('project_comments')
.select(`
comment_id,
project_id,
user_id,
projects!inner(created_by)
`)
.eq('comment_id', commentId)
.single();
if (fetchError || !comment) {
return c.json({ error: 'Comment not found' }, 404);
}
// 确保我们能够安全地访问projects中的created_by字段
const projectOwner = comment.projects &&
Array.isArray(comment.projects) &&
comment.projects.length > 0 ?
comment.projects[0].created_by : null;
// 检查用户是否有权限删除评论
const isCommentOwner = comment.user_id === user.id;
const isProjectOwner = projectOwner === user.id;
if (!isCommentOwner && !isProjectOwner) {
return c.json({
error: 'You do not have permission to delete this comment'
}, 403);
}
// 删除评论
const { error } = await supabase_1.default
.from('project_comments')
.delete()
.eq('comment_id', commentId);
if (error) {
console.error('Error deleting project comment:', error);
return c.json({ error: 'Failed to delete comment' }, 500);
}
return c.json({
message: 'Comment deleted successfully'
});
}
catch (error) {
console.error('Error deleting project comment:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// 获取项目评论的统计信息
projectCommentsRouter.get('/projects/:id/comments/stats', async (c) => {
try {
const projectId = c.req.param('id');
// 检查项目是否存在
const { data: project, error: projectError } = await supabase_1.default
.from('projects')
.select('id, name')
.eq('id', projectId)
.single();
if (projectError) {
console.error('Error fetching project:', projectError);
return c.json({ error: 'Project not found' }, 404);
}
// 从Supabase获取评论总数
const { count } = await supabase_1.default
.from('project_comments')
.select('*', { count: 'exact', head: true })
.eq('project_id', projectId);
// 从Supabase获取情感分析统计
const { data: sentimentStats } = await supabase_1.default
.from('project_comments')
.select('sentiment_score')
.eq('project_id', projectId);
let averageSentiment = 0;
let positiveCount = 0;
let neutralCount = 0;
let negativeCount = 0;
if (sentimentStats && sentimentStats.length > 0) {
// 计算平均情感分数
const totalSentiment = sentimentStats.reduce((acc, curr) => acc + (curr.sentiment_score || 0), 0);
averageSentiment = totalSentiment / sentimentStats.length;
// 分类情感分数
sentimentStats.forEach(stat => {
const score = stat.sentiment_score || 0;
if (score > 0.3) {
positiveCount++;
}
else if (score < -0.3) {
negativeCount++;
}
else {
neutralCount++;
}
});
}
let timeTrend = [];
try {
const result = await clickhouse_1.default.query({
query: `
SELECT
toDate(timestamp) as date,
count() as comment_count
FROM events
WHERE
project_id = ? AND
event_type = 'project_comment' AND
timestamp >= subtractDays(now(), 30)
GROUP BY date
ORDER BY date ASC
`,
values: [projectId]
});
timeTrend = 'rows' in result ? result.rows : [];
}
catch (chError) {
console.error('Error fetching comment time trend:', chError);
// 继续执行,返回空趋势数据
}
return c.json({
project_id: projectId,
project_name: project.name,
total_comments: count || 0,
sentiment: {
average: averageSentiment,
positive: positiveCount,
neutral: neutralCount,
negative: negativeCount
},
time_trend: timeTrend
});
}
catch (error) {
console.error('Error fetching project comment stats:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
exports.default = projectCommentsRouter;

File diff suppressed because it is too large Load Diff

View File

@@ -1,87 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.initClickHouse = void 0;
const client_1 = require("@clickhouse/client");
const config_1 = __importDefault(require("../config"));
// Create ClickHouse client with error handling
const createClickHouseClient = () => {
try {
return (0, client_1.createClient)({
host: `http://${config_1.default.clickhouse.host}:${config_1.default.clickhouse.port}`,
username: config_1.default.clickhouse.user,
password: config_1.default.clickhouse.password,
database: config_1.default.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 }) => {
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
const initClickHouse = async () => {
try {
// Create database if not exists
await clickhouse.query({
query: `CREATE DATABASE IF NOT EXISTS ${config_1.default.clickhouse.database}`,
});
// Create tables for tracking events
await clickhouse.query({
query: `
CREATE TABLE IF NOT EXISTS ${config_1.default.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_1.default.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_1.default.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...');
}
};
exports.initClickHouse = initClickHouse;
exports.default = clickhouse;

View File

@@ -1,492 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.initDatabase = exports.checkDatabaseConnection = exports.createSampleData = exports.initSupabaseFunctions = exports.initClickHouseTables = exports.initSupabaseTables = void 0;
const supabase_1 = __importDefault(require("./supabase"));
const clickhouse_1 = __importDefault(require("./clickhouse"));
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
/**
* 初始化 Supabase (PostgreSQL) 数据库表
*/
const initSupabaseTables = async () => {
try {
console.log('开始初始化 Supabase 数据表...');
// 创建用户扩展表
await supabase_1.default.rpc('create_user_profiles_if_not_exists');
// 创建项目表
await supabase_1.default.rpc('create_projects_table_if_not_exists');
// 创建网红(影响者)表
await supabase_1.default.rpc('create_influencers_table_if_not_exists');
// 创建项目-网红关联表
await supabase_1.default.rpc('create_project_influencers_table_if_not_exists');
// 创建帖子表
await supabase_1.default.rpc('create_posts_table_if_not_exists');
// 创建评论表
await supabase_1.default.rpc('create_comments_table_if_not_exists');
// 创建项目评论表
await supabase_1.default.rpc('create_project_comments_table_if_not_exists');
console.log('Supabase 数据表初始化完成');
return true;
}
catch (error) {
console.error('初始化 Supabase 数据表失败:', error);
return false;
}
};
exports.initSupabaseTables = initSupabaseTables;
/**
* 初始化 ClickHouse 数据库表
*/
const initClickHouseTables = async () => {
try {
console.log('开始初始化 ClickHouse 数据表...');
// 创建事件表
await clickhouse_1.default.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_1.default.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_1.default.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_1.default.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_1.default.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;
}
};
exports.initClickHouseTables = initClickHouseTables;
/**
* 初始化 Supabase 存储函数
*/
const initSupabaseFunctions = async () => {
try {
console.log('开始初始化 Supabase 存储过程...');
// 创建用户简档表的存储过程
await supabase_1.default.rpc('create_function_create_user_profiles_if_not_exists');
// 创建项目表的存储过程
await supabase_1.default.rpc('create_function_create_projects_table_if_not_exists');
// 创建网红表的存储过程
await supabase_1.default.rpc('create_function_create_influencers_table_if_not_exists');
// 创建项目-网红关联表的存储过程
await supabase_1.default.rpc('create_function_create_project_influencers_table_if_not_exists');
// 创建帖子表的存储过程
await supabase_1.default.rpc('create_function_create_posts_table_if_not_exists');
// 创建评论表的存储过程
await supabase_1.default.rpc('create_function_create_comments_table_if_not_exists');
// 创建项目评论表的存储过程
await supabase_1.default.rpc('create_function_create_project_comments_table_if_not_exists');
// 创建评论相关的SQL函数
console.log('创建评论相关的SQL函数...');
const commentsSQL = await promises_1.default.readFile(path_1.default.join(__dirname, 'supabase-comments-functions.sql'), 'utf8');
// 使用Supabase执行SQL
const { error: commentsFunctionsError } = await supabase_1.default.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;
}
};
exports.initSupabaseFunctions = initSupabaseFunctions;
/**
* 创建测试数据
*/
const createSampleData = async () => {
try {
console.log('开始创建测试数据...');
// 创建测试用户
const { data: user, error: userError } = await supabase_1.default.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_1.default
.from('projects')
.insert({
name: '测试营销活动',
description: '这是一个测试营销活动',
created_by: user.user.id
})
.select()
.single();
if (projectError) {
console.error('创建测试项目失败:', projectError);
return false;
}
// 创建项目评论
await supabase_1.default
.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_1.default
.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_1.default
.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_1.default
.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_1.default
.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_1.default.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_1.default.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_1.default.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_1.default.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_1.default.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;
}
};
exports.createSampleData = createSampleData;
/**
* 检查数据库连接
*/
const checkDatabaseConnection = async () => {
try {
console.log('检查数据库连接...');
// 检查 Supabase 连接
try {
// 仅检查连接是否正常,不执行实际查询
const { data, error } = await supabase_1.default.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_1.default.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;
}
};
exports.checkDatabaseConnection = checkDatabaseConnection;
/**
* 初始化数据库 - 此函数现在仅作为手动初始化的入口点
* 只有通过管理API明确调用时才会执行实际的初始化
*/
const initDatabase = async () => {
try {
console.log('开始数据库初始化...');
console.log('警告: 此操作将修改数据库结构,请确保您知道自己在做什么');
// 初始化 Supabase 函数
await (0, exports.initSupabaseFunctions)();
// 初始化 Supabase 表
await (0, exports.initSupabaseTables)();
// 初始化 ClickHouse 表
await (0, exports.initClickHouseTables)();
console.log('数据库初始化完成');
return true;
}
catch (error) {
console.error('数据库初始化失败:', error);
return false;
}
};
exports.initDatabase = initDatabase;

View File

@@ -1,158 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.addNotificationJob = exports.addAnalyticsJob = exports.initWorkers = exports.QUEUE_NAMES = void 0;
const bullmq_1 = require("bullmq");
const config_1 = __importDefault(require("../config"));
// Define queue names
exports.QUEUE_NAMES = {
ANALYTICS: 'analytics',
NOTIFICATIONS: 'notifications',
};
// Create Redis connection options
const redisOptions = {
host: config_1.default.bull.redis.host,
port: config_1.default.bull.redis.port,
password: config_1.default.bull.redis.password,
};
// Create queues with error handling
let analyticsQueue;
let notificationsQueue;
try {
analyticsQueue = new bullmq_1.Queue(exports.QUEUE_NAMES.ANALYTICS, {
connection: redisOptions,
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000,
},
},
});
notificationsQueue = new bullmq_1.Queue(exports.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, data) => {
console.log(`Mock analytics job added: ${name}`, data);
return { id: 'mock-job-id' };
},
close: async () => console.log('Mock analytics queue closed'),
};
notificationsQueue = {
add: async (name, data) => {
console.log(`Mock notification job added: ${name}`, data);
return { id: 'mock-job-id' };
},
close: async () => console.log('Mock notifications queue closed'),
};
}
// Initialize workers
const initWorkers = () => {
try {
// Analytics worker
const analyticsWorker = new bullmq_1.Worker(exports.QUEUE_NAMES.ANALYTICS, async (job) => {
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}`);
}
}, { connection: redisOptions });
// Notifications worker
const notificationsWorker = new bullmq_1.Worker(exports.QUEUE_NAMES.NOTIFICATIONS, async (job) => {
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}`);
}
}, { connection: redisOptions });
// Handle worker events
analyticsWorker.on('completed', (job) => {
console.log(`Analytics job ${job.id} completed`);
});
analyticsWorker.on('failed', (job, err) => {
console.error(`Analytics job ${job?.id} failed with error ${err.message}`);
});
notificationsWorker.on('completed', (job) => {
console.log(`Notification job ${job.id} completed`);
});
notificationsWorker.on('failed', (job, err) => {
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'),
},
};
}
};
exports.initWorkers = initWorkers;
// Helper function to add jobs to queues
const addAnalyticsJob = async (type, data, options = {}) => {
try {
return await analyticsQueue.add(type, { type, data }, options);
}
catch (error) {
console.error('Error adding analytics job:', error);
console.log('Job details:', { type, data });
return null;
}
};
exports.addAnalyticsJob = addAnalyticsJob;
const addNotificationJob = async (type, data, options = {}) => {
try {
return await notificationsQueue.add(type, { type, data }, options);
}
catch (error) {
console.error('Error adding notification job:', error);
console.log('Job details:', { type, data });
return null;
}
};
exports.addNotificationJob = addNotificationJob;

View File

@@ -1,80 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getRedisClient = exports.connectRedis = exports.redisClient = void 0;
const redis_1 = require("redis");
const config_1 = __importDefault(require("../config"));
// Create Redis client
const redisClient = (0, redis_1.createClient)({
url: `redis://${config_1.default.redis.password ? `${config_1.default.redis.password}@` : ''}${config_1.default.redis.host}:${config_1.default.redis.port}`,
});
exports.redisClient = redisClient;
// 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();
return {
isOpen: true,
connect: async () => console.log('Mock Redis client connected'),
get: async (key) => store.get(key) || null,
set: async (key, value) => {
store.set(key, value);
return 'OK';
},
incr: async (key) => {
const current = parseInt(store.get(key) || '0', 10);
const newValue = current + 1;
store.set(key, newValue.toString());
return newValue;
},
decr: async (key) => {
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 = 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;
}
};
exports.connectRedis = connectRedis;
// Export the appropriate client
const getRedisClient = async () => {
try {
if (redisClient.isOpen) {
return redisClient;
}
return await connectRedis();
}
catch (error) {
if (!mockRedisClient) {
mockRedisClient = createMockRedisClient();
}
return mockRedisClient;
}
};
exports.getRedisClient = getRedisClient;
exports.default = redisClient;

View File

@@ -1,18 +0,0 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const supabase_js_1 = require("@supabase/supabase-js");
const config_1 = __importDefault(require("../config"));
// Validate Supabase URL
const validateSupabaseUrl = (url) => {
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 = (0, supabase_js_1.createClient)(validateSupabaseUrl(config_1.default.supabase.url), config_1.default.supabase.key || 'dummy-key');
exports.default = supabase;

View File

@@ -15,6 +15,7 @@ import { initClickHouse } from './utils/clickhouse';
import { initWorkers } from './utils/queue'; import { initWorkers } from './utils/queue';
import { initDatabase, createSampleData, checkDatabaseConnection } from './utils/initDatabase'; import { initDatabase, createSampleData, checkDatabaseConnection } from './utils/initDatabase';
import { createSwaggerUI } from './swagger'; import { createSwaggerUI } from './swagger';
import { initScheduledTaskWorkers } from './utils/scheduledTasks';
// Create Hono app // Create Hono app
const app = new Hono(); const app = new Hono();
@@ -119,16 +120,12 @@ const startServer = async () => {
console.log('NOTICE: Database will NOT be automatically initialized on startup'); console.log('NOTICE: Database will NOT be automatically initialized on startup');
console.log('Use /api/admin/init-db endpoint to manually initialize the database if needed'); console.log('Use /api/admin/init-db endpoint to manually initialize the database if needed');
// Initialize BullMQ workers // Initialize workers for background processing
let workers; console.log('🏗️ Initializing workers...');
try { const workers = {
workers = initWorkers(); backgroundWorkers: initWorkers(),
console.log('BullMQ workers initialized'); scheduledTaskWorker: initScheduledTaskWorkers()
} catch (error) { };
console.error('Failed to initialize BullMQ workers:', error);
console.log('Background processing will not be available...');
workers = { analyticsWorker: null, notificationsWorker: null };
}
// Start server // Start server
const port = Number(config.port); const port = Number(config.port);
@@ -149,12 +146,16 @@ const startServer = async () => {
console.log('Shutting down server...'); console.log('Shutting down server...');
// Close workers if they exist // Close workers if they exist
if (workers.analyticsWorker) { if (workers.backgroundWorkers.analyticsWorker) {
await workers.analyticsWorker.close(); await workers.backgroundWorkers.analyticsWorker.close();
} }
if (workers.notificationsWorker) { if (workers.backgroundWorkers.notificationsWorker) {
await workers.notificationsWorker.close(); await workers.backgroundWorkers.notificationsWorker.close();
}
if (workers.scheduledTaskWorker) {
await workers.scheduledTaskWorker.close();
} }
process.exit(0); process.exit(0);

View File

@@ -4,6 +4,12 @@ import clickhouse from '../utils/clickhouse';
import { addAnalyticsJob } from '../utils/queue'; import { addAnalyticsJob } from '../utils/queue';
import { getRedisClient } from '../utils/redis'; import { getRedisClient } from '../utils/redis';
import supabase from '../utils/supabase'; import supabase from '../utils/supabase';
import {
scheduleInfluencerCollection,
schedulePostCollection,
removeScheduledJob,
getScheduledJobs
} from '../utils/scheduledTasks';
// Define user type // Define user type
interface User { interface User {
@@ -519,4 +525,357 @@ analyticsRouter.get('/project/:id/interaction-types', async (c) => {
} }
}); });
// ===== Scheduled Collection Endpoints =====
// Schedule automated data collection for an influencer
analyticsRouter.post('/schedule/influencer', async (c) => {
try {
const { influencer_id, cron_expression } = await c.req.json();
if (!influencer_id) {
return c.json({ error: 'Influencer ID is required' }, 400);
}
// Validate that the influencer exists
const { data, error } = await supabase
.from('influencers')
.select('influencer_id')
.eq('influencer_id', influencer_id)
.single();
if (error || !data) {
return c.json({ error: 'Influencer not found' }, 404);
}
// Schedule the collection job
await scheduleInfluencerCollection(
influencer_id,
cron_expression || '0 0 * * *' // Default: Every day at midnight
);
return c.json({
message: 'Influencer metrics collection scheduled successfully',
influencer_id,
cron_expression: cron_expression || '0 0 * * *'
});
} catch (error) {
console.error('Error scheduling influencer collection:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// Schedule automated data collection for a post
analyticsRouter.post('/schedule/post', async (c) => {
try {
const { post_id, cron_expression } = await c.req.json();
if (!post_id) {
return c.json({ error: 'Post ID is required' }, 400);
}
// Validate that the post exists
const { data, error } = await supabase
.from('posts')
.select('post_id')
.eq('post_id', post_id)
.single();
if (error || !data) {
return c.json({ error: 'Post not found' }, 404);
}
// Schedule the collection job
await schedulePostCollection(
post_id,
cron_expression || '0 0 * * *' // Default: Every day at midnight
);
return c.json({
message: 'Post metrics collection scheduled successfully',
post_id,
cron_expression: cron_expression || '0 0 * * *'
});
} catch (error) {
console.error('Error scheduling post collection:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// Get all scheduled collection jobs
analyticsRouter.get('/schedule', async (c) => {
try {
const scheduledJobs = await getScheduledJobs();
return c.json({
scheduled_jobs: scheduledJobs
});
} catch (error) {
console.error('Error fetching scheduled jobs:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// Delete a scheduled collection job
analyticsRouter.delete('/schedule/:job_id', async (c) => {
try {
const jobId = c.req.param('job_id');
await removeScheduledJob(jobId);
return c.json({
message: 'Scheduled job removed successfully',
job_id: jobId
});
} catch (error) {
console.error('Error removing scheduled job:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// ===== Data Export Endpoints =====
// Export influencer growth data (CSV format)
analyticsRouter.get('/export/influencer/:id/growth', async (c) => {
try {
const influencerId = c.req.param('id');
const {
metric = 'followers_count',
timeframe = '6months',
interval = 'month'
} = c.req.query();
// The same logic as the influencer growth endpoint, but return CSV format
// Validate parameters
const validMetrics = ['followers_count', 'video_count', 'views_count', 'likes_count'];
if (!validMetrics.includes(metric)) {
return c.json({ error: 'Invalid metric specified' }, 400);
}
// Determine time range and interval function
let timeRangeSql: string;
let intervalFunction: string;
switch (timeframe) {
case '30days':
timeRangeSql = 'timestamp >= subtractDays(now(), 30)';
break;
case '90days':
timeRangeSql = 'timestamp >= subtractDays(now(), 90)';
break;
case '6months':
default:
timeRangeSql = 'timestamp >= subtractMonths(now(), 6)';
break;
case '1year':
timeRangeSql = 'timestamp >= subtractYears(now(), 1)';
break;
}
switch (interval) {
case 'day':
intervalFunction = 'toDate(timestamp)';
break;
case 'week':
intervalFunction = 'toStartOfWeek(timestamp)';
break;
case 'month':
default:
intervalFunction = 'toStartOfMonth(timestamp)';
break;
}
// Query ClickHouse for data
const result = await clickhouse.query({
query: `
SELECT
${intervalFunction} AS time_period,
sumIf(metric_value, metric_name = ?) AS change,
maxIf(metric_total, metric_name = ?) AS total_value
FROM promote.events
WHERE
influencer_id = ? AND
event_type = ? AND
${timeRangeSql}
GROUP BY time_period
ORDER BY time_period ASC
`,
values: [
metric,
metric,
influencerId,
`${metric}_change`
]
});
// Get influencer info
const { data: influencer } = await supabase
.from('influencers')
.select('name, platform')
.eq('influencer_id', influencerId)
.single();
// Extract trend data
const trendData = 'rows' in result ? result.rows : [];
// Format as CSV
const csvHeader = `Time Period,Change,Total Value\n`;
const csvRows = trendData.map((row: any) =>
`${row.time_period},${row.change},${row.total_value}`
).join('\n');
const influencerInfo = influencer
? `Influencer: ${influencer.name} (${influencer.platform})\nMetric: ${metric}\nTimeframe: ${timeframe}\nInterval: ${interval}\n\n`
: '';
const csvContent = influencerInfo + csvHeader + csvRows;
return c.body(csvContent, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': `attachment; filename="influencer_growth_${influencerId}.csv"`
}
});
} catch (error) {
console.error('Error exporting influencer growth data:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
// Export project performance data (CSV format)
analyticsRouter.get('/export/project/:id/performance', async (c) => {
try {
const projectId = c.req.param('id');
const { timeframe = '30days' } = c.req.query();
// Get project information
const { data: project, error: projectError } = await supabase
.from('projects')
.select('id, name, description')
.eq('id', projectId)
.single();
if (projectError) {
return c.json({ error: 'Project not found' }, 404);
}
// Get project influencers
const { data: projectInfluencers, error: influencersError } = await supabase
.from('project_influencers')
.select('influencer_id')
.eq('project_id', projectId);
if (influencersError) {
console.error('Error fetching project influencers:', influencersError);
return c.json({ error: 'Failed to fetch project data' }, 500);
}
const influencerIds = projectInfluencers.map(pi => pi.influencer_id);
if (influencerIds.length === 0) {
const emptyCSV = `Project: ${project.name}\nNo influencers found in this project.`;
return c.body(emptyCSV, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': `attachment; filename="project_performance_${projectId}.csv"`
}
});
}
// Determine time range
let startDate: Date;
const endDate = new Date();
switch (timeframe) {
case '7days':
startDate = new Date(endDate);
startDate.setDate(endDate.getDate() - 7);
break;
case '30days':
default:
startDate = new Date(endDate);
startDate.setDate(endDate.getDate() - 30);
break;
case '90days':
startDate = new Date(endDate);
startDate.setDate(endDate.getDate() - 90);
break;
case '6months':
startDate = new Date(endDate);
startDate.setMonth(endDate.getMonth() - 6);
break;
}
// Get influencer details
const { data: influencersData } = await supabase
.from('influencers')
.select('influencer_id, name, platform, followers_count')
.in('influencer_id', influencerIds);
// Get metrics from ClickHouse
const metricsResult = await clickhouse.query({
query: `
SELECT
influencer_id,
sumIf(metric_value, event_type = 'followers_count_change') AS followers_change,
sumIf(metric_value, event_type = 'post_views_count_change') AS views_change,
sumIf(metric_value, event_type = 'post_likes_count_change') AS likes_change
FROM promote.events
WHERE
influencer_id IN (?) AND
timestamp >= ? AND
timestamp <= ?
GROUP BY influencer_id
`,
values: [
influencerIds,
startDate.toISOString(),
endDate.toISOString()
]
});
// Extract metrics data
const metricsData = 'rows' in metricsResult ? metricsResult.rows : [];
// Combine data
const reportData = (influencersData || []).map(influencer => {
const metrics = metricsData.find((m: any) => m.influencer_id === influencer.influencer_id) || {
followers_change: 0,
views_change: 0,
likes_change: 0
};
return {
influencer_id: influencer.influencer_id,
name: influencer.name,
platform: influencer.platform,
followers_count: influencer.followers_count,
followers_change: metrics.followers_change || 0,
views_change: metrics.views_change || 0,
likes_change: metrics.likes_change || 0
};
});
// Format as CSV
const csvHeader = `Influencer Name,Platform,Followers Count,Followers Change,Views Change,Likes Change\n`;
const csvRows = reportData.map(row =>
`${row.name},${row.platform},${row.followers_count},${row.followers_change},${row.views_change},${row.likes_change}`
).join('\n');
const projectInfo = `Project: ${project.name}\nDescription: ${project.description || 'N/A'}\nTimeframe: ${timeframe}\nExport Date: ${new Date().toISOString()}\n\n`;
const csvContent = projectInfo + csvHeader + csvRows;
return c.body(csvContent, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': `attachment; filename="project_performance_${projectId}.csv"`
}
});
} catch (error) {
console.error('Error exporting project performance data:', error);
return c.json({ error: 'Internal server error' }, 500);
}
});
export default analyticsRouter; export default analyticsRouter;

View File

View File

@@ -46,6 +46,28 @@ export const initClickHouseTables = async () => {
try { try {
console.log('开始初始化 ClickHouse 数据表...'); console.log('开始初始化 ClickHouse 数据表...');
// Create events table for general analytics tracking
await clickhouse.query({
query: `
CREATE TABLE IF NOT EXISTS promote.events (
event_id UUID DEFAULT generateUUIDv4(),
event_type String,
influencer_id String DEFAULT '',
post_id String DEFAULT '',
project_id String DEFAULT '',
timestamp DateTime64(3) DEFAULT now64(3),
metric_name String DEFAULT '',
metric_value Int64 DEFAULT 0,
metric_total Int64 DEFAULT 0,
recorded_by String DEFAULT '',
extra_data String DEFAULT ''
)
ENGINE = MergeTree()
ORDER BY (event_type, influencer_id, post_id, timestamp)
`
});
console.log(' - Created events table');
// 创建事件表 // 创建事件表
await clickhouse.query({ await clickhouse.query({
query: ` query: `

View File

@@ -0,0 +1,406 @@
import { Queue, Worker } from 'bullmq';
import supabase from './supabase';
import clickhouse from './clickhouse';
import { getRedisClient } from './redis';
interface ScheduledCollectionData {
type: 'influencer_metrics' | 'post_metrics';
influencer_id?: string;
post_id?: string;
project_id?: string;
}
// Create a mock scheduler if BullMQ doesn't export QueueScheduler
class MockQueueScheduler {
constructor(queueName: string, options: any) {
console.log(`Creating mock scheduler for queue: ${queueName}`);
}
}
// Create scheduled collection queue
const createScheduledTaskQueue = async () => {
const connection = {
host: process.env.BULL_REDIS_HOST || 'localhost',
port: parseInt(process.env.BULL_REDIS_PORT || '6379'),
password: process.env.BULL_REDIS_PASSWORD || '',
};
const queueOptions = {
connection,
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 5000,
},
},
};
// Create queue
const scheduledCollectionQueue = new Queue('scheduled-data-collection', queueOptions);
// Note about scheduler:
// In a production environment, a QueueScheduler should be initialized
// for handling delayed/repeatable jobs properly.
// The QueueScheduler can be initialized separately if needed.
return scheduledCollectionQueue;
};
// Initialize scheduled collection workers
export const initScheduledTaskWorkers = () => {
const worker = new Worker(
'scheduled-data-collection',
async (job) => {
console.log(`Processing scheduled task: ${job.id}`, job.data);
const { type, influencer_id, post_id, project_id } = job.data as ScheduledCollectionData;
try {
if (type === 'influencer_metrics') {
await collectInfluencerMetrics(influencer_id);
} else if (type === 'post_metrics') {
await collectPostMetrics(post_id);
}
console.log(`Successfully completed scheduled task: ${job.id}`);
return { success: true, timestamp: new Date().toISOString() };
} catch (error) {
console.error(`Error processing scheduled task ${job.id}:`, error);
throw error;
}
},
{
connection: {
host: process.env.BULL_REDIS_HOST || 'localhost',
port: parseInt(process.env.BULL_REDIS_PORT || '6379'),
password: process.env.BULL_REDIS_PASSWORD || '',
},
concurrency: 5,
}
);
worker.on('completed', job => {
console.log(`Scheduled task completed: ${job.id}`);
});
worker.on('failed', (job, err) => {
console.error(`Scheduled task failed: ${job?.id}`, err);
});
return worker;
};
// Schedule data collection jobs
export const scheduleInfluencerCollection = async (
influencerId: string,
cronExpression: string = '0 0 * * *' // Default: Every day at midnight
) => {
const queue = await createScheduledTaskQueue();
await queue.add(
`influencer-collection-${influencerId}`,
{
type: 'influencer_metrics',
influencer_id: influencerId,
scheduled_at: new Date().toISOString()
},
{
jobId: `influencer-${influencerId}-${Date.now()}`,
repeat: {
pattern: cronExpression
}
}
);
return true;
};
export const schedulePostCollection = async (
postId: string,
cronExpression: string = '0 0 * * *' // Default: Every day at midnight
) => {
const queue = await createScheduledTaskQueue();
await queue.add(
`post-collection-${postId}`,
{
type: 'post_metrics',
post_id: postId,
scheduled_at: new Date().toISOString()
},
{
jobId: `post-${postId}-${Date.now()}`,
repeat: {
pattern: cronExpression
}
}
);
return true;
};
// Remove scheduled jobs
export const removeScheduledJob = async (jobId: string) => {
const queue = await createScheduledTaskQueue();
await queue.removeRepeatableByKey(jobId);
return true;
};
// Get all scheduled jobs
export const getScheduledJobs = async () => {
const queue = await createScheduledTaskQueue();
const repeatableJobs = await queue.getRepeatableJobs();
return repeatableJobs;
};
// Implementation of collection functions
// These functions would typically call APIs or scrape data from platforms
async function collectInfluencerMetrics(influencerId?: string) {
if (!influencerId) {
throw new Error('Influencer ID is required');
}
// Get influencer data from Supabase
const { data: influencer, error } = await supabase
.from('influencers')
.select('influencer_id, name, platform, external_id')
.eq('influencer_id', influencerId)
.single();
if (error || !influencer) {
throw new Error(`Failed to find influencer: ${error?.message}`);
}
// Here you would integrate with platform APIs to get updated metrics
// This is a placeholder that would be replaced with actual API calls
// Simulate collecting metrics (in a real scenario, this would come from APIs)
const simulatedMetrics = {
followers_count: Math.floor(50000 + Math.random() * 1000),
video_count: Math.floor(100 + Math.random() * 5),
views_count: Math.floor(1000000 + Math.random() * 50000),
likes_count: Math.floor(500000 + Math.random() * 20000)
};
// Record the metrics in both Supabase and ClickHouse
// Get the current metrics to calculate changes
const { data: currentMetrics, error: metricsError } = await supabase
.from('influencers')
.select('followers_count, video_count')
.eq('influencer_id', influencerId)
.single();
if (metricsError) {
throw new Error(`Failed to get current metrics: ${metricsError.message}`);
}
// Calculate changes
const followerChange = (simulatedMetrics.followers_count || 0) - (currentMetrics?.followers_count || 0);
const videoChange = (simulatedMetrics.video_count || 0) - (currentMetrics?.video_count || 0);
// Update Supabase
const { error: updateError } = await supabase
.from('influencers')
.update(simulatedMetrics)
.eq('influencer_id', influencerId);
if (updateError) {
throw new Error(`Failed to update influencer metrics: ${updateError.message}`);
}
// Record events in ClickHouse
const timestamp = new Date().toISOString();
const eventPromises = [];
if (followerChange !== 0) {
eventPromises.push(
clickhouse.query({
query: `
INSERT INTO promote.events (
event_type,
influencer_id,
timestamp,
metric_name,
metric_value,
metric_total,
recorded_by
) VALUES (?, ?, ?, ?, ?, ?, ?)
`,
values: [
'followers_count_change',
influencerId,
timestamp,
'followers_count',
followerChange,
simulatedMetrics.followers_count,
'system' // Recorded by the system scheduler
]
})
);
}
if (videoChange !== 0) {
eventPromises.push(
clickhouse.query({
query: `
INSERT INTO promote.events (
event_type,
influencer_id,
timestamp,
metric_name,
metric_value,
metric_total,
recorded_by
) VALUES (?, ?, ?, ?, ?, ?, ?)
`,
values: [
'video_count_change',
influencerId,
timestamp,
'video_count',
videoChange,
simulatedMetrics.video_count,
'system' // Recorded by the system scheduler
]
})
);
}
await Promise.all(eventPromises);
return {
influencer_id: influencerId,
timestamp,
metrics: simulatedMetrics,
changes: {
followers: followerChange,
videos: videoChange
}
};
}
async function collectPostMetrics(postId?: string) {
if (!postId) {
throw new Error('Post ID is required');
}
// Get post data from Supabase
const { data: post, error } = await supabase
.from('posts')
.select('post_id, influencer_id, platform, post_url, title')
.eq('post_id', postId)
.single();
if (error || !post) {
throw new Error(`Failed to find post: ${error?.message}`);
}
// Here you would integrate with platform APIs to get updated metrics
// This is a placeholder that would be replaced with actual API calls
// Simulate collecting metrics (in a real scenario, this would come from APIs)
const simulatedMetrics = {
views_count: Math.floor(10000 + Math.random() * 5000),
likes_count: Math.floor(5000 + Math.random() * 1000),
comments_count: Math.floor(200 + Math.random() * 50),
shares_count: Math.floor(100 + Math.random() * 20)
};
// Get the current metrics to calculate changes
const { data: currentMetrics, error: metricsError } = await supabase
.from('posts')
.select('views_count, likes_count, comments_count, shares_count')
.eq('post_id', postId)
.single();
if (metricsError) {
throw new Error(`Failed to get current metrics: ${metricsError.message}`);
}
// Calculate changes
const viewsChange = (simulatedMetrics.views_count || 0) - (currentMetrics?.views_count || 0);
const likesChange = (simulatedMetrics.likes_count || 0) - (currentMetrics?.likes_count || 0);
const commentsChange = (simulatedMetrics.comments_count || 0) - (currentMetrics?.comments_count || 0);
const sharesChange = (simulatedMetrics.shares_count || 0) - (currentMetrics?.shares_count || 0);
// Update Supabase
const { error: updateError } = await supabase
.from('posts')
.update(simulatedMetrics)
.eq('post_id', postId);
if (updateError) {
throw new Error(`Failed to update post metrics: ${updateError.message}`);
}
// Record events in ClickHouse
const timestamp = new Date().toISOString();
const eventPromises = [];
// Only record changes if they are non-zero
interface MetricChanges {
views: number;
likes: number;
comments: number;
shares: number;
}
const changes: MetricChanges = {
views: viewsChange,
likes: likesChange,
comments: commentsChange,
shares: sharesChange
};
const metricsMap = {
views: simulatedMetrics.views_count,
likes: simulatedMetrics.likes_count,
comments: simulatedMetrics.comments_count,
shares: simulatedMetrics.shares_count
};
for (const [key, value] of Object.entries(changes)) {
if (value !== 0) {
const metricName = `${key}_count`;
const metricTotal = metricsMap[key as keyof typeof metricsMap];
eventPromises.push(
clickhouse.query({
query: `
INSERT INTO promote.events (
event_type,
post_id,
influencer_id,
timestamp,
metric_name,
metric_value,
metric_total,
recorded_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`,
values: [
`post_${metricName}_change`,
postId,
post.influencer_id,
timestamp,
metricName,
value,
metricTotal,
'system' // Recorded by the system scheduler
]
})
);
}
}
await Promise.all(eventPromises);
return {
post_id: postId,
timestamp,
metrics: simulatedMetrics,
changes
};
}