init
This commit is contained in:
649
backend/dist/routes/community.js
vendored
Normal file
649
backend/dist/routes/community.js
vendored
Normal file
@@ -0,0 +1,649 @@
|
||||
"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;
|
||||
Reference in New Issue
Block a user