cleaning up api
This commit is contained in:
@@ -1,27 +1,43 @@
|
|||||||
import { Context } from 'hono';
|
import { Context } from 'hono';
|
||||||
import supabase from '../utils/supabase';
|
import supabase from '../utils/supabase';
|
||||||
|
|
||||||
|
// Get all comments with filtering and pagination
|
||||||
export const getComments = async (c: Context) => {
|
export const getComments = async (c: Context) => {
|
||||||
try {
|
try {
|
||||||
const { post_id, limit = '10', offset = '0' } = c.req.query();
|
const { post_id, limit = '10', offset = '0' } = c.req.query();
|
||||||
|
|
||||||
let query;
|
let query = supabase
|
||||||
|
.from('comments')
|
||||||
|
.select(`
|
||||||
|
comment_id,
|
||||||
|
post_id,
|
||||||
|
user_id,
|
||||||
|
content,
|
||||||
|
sentiment_score,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
posts (
|
||||||
|
post_id,
|
||||||
|
title,
|
||||||
|
platform
|
||||||
|
)
|
||||||
|
`, { count: 'exact' });
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
if (post_id) {
|
if (post_id) {
|
||||||
// 获取特定帖子的评论
|
query = query.eq('post_id', post_id);
|
||||||
query = supabase.rpc('get_comments_for_post', { post_id_param: post_id });
|
|
||||||
} else {
|
|
||||||
// 获取所有评论
|
|
||||||
query = supabase.rpc('get_comments_with_posts');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用分页
|
// Apply pagination and sorting
|
||||||
query = query.range(Number(offset), Number(offset) + Number(limit) - 1);
|
query = query
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.range(Number(offset), Number(offset) + Number(limit) - 1);
|
||||||
|
|
||||||
const { data: comments, error, count } = await query;
|
const { data: comments, error, count } = await query;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return c.json({ error: error.message }, 500);
|
console.error('Error fetching comments:', error);
|
||||||
|
return c.json({ error: 'Failed to fetch comments' }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
@@ -31,90 +47,169 @@ export const getComments = async (c: Context) => {
|
|||||||
offset: Number(offset)
|
offset: Number(offset)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error in getComments:', error);
|
||||||
return c.json({ error: 'Internal server error' }, 500);
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createComment = async (c: Context) => {
|
// Get a specific comment by ID
|
||||||
|
export const getCommentById = async (c: Context) => {
|
||||||
try {
|
try {
|
||||||
const { post_id, content } = await c.req.json();
|
const commentId = c.req.param('comment_id');
|
||||||
const user_id = c.get('user')?.id;
|
|
||||||
|
|
||||||
if (!user_id) {
|
|
||||||
return c.json({ error: 'Unauthorized' }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: comment, error } = await supabase
|
const { data: comment, error } = await supabase
|
||||||
.from('comments')
|
.from('comments')
|
||||||
.insert({
|
|
||||||
post_id,
|
|
||||||
content,
|
|
||||||
user_id
|
|
||||||
})
|
|
||||||
.select(`
|
.select(`
|
||||||
comment_id,
|
comment_id,
|
||||||
|
post_id,
|
||||||
|
user_id,
|
||||||
content,
|
content,
|
||||||
sentiment_score,
|
sentiment_score,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
post_id,
|
posts (
|
||||||
user_id
|
post_id,
|
||||||
|
title,
|
||||||
|
platform
|
||||||
|
)
|
||||||
`)
|
`)
|
||||||
.single();
|
.eq('comment_id', commentId)
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return c.json({ error: error.message }, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户信息
|
|
||||||
const { data: userProfile, error: userError } = await supabase
|
|
||||||
.from('user_profiles')
|
|
||||||
.select('id, full_name, avatar_url')
|
|
||||||
.eq('id', user_id)
|
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (!userError && userProfile) {
|
if (error) {
|
||||||
comment.user_profile = userProfile;
|
console.error('Error fetching comment:', error);
|
||||||
|
return c.json({ error: 'Failed to fetch comment' }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json(comment, 201);
|
if (!comment) {
|
||||||
|
return c.json({ error: 'Comment not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(comment);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error in getCommentById:', error);
|
||||||
return c.json({ error: 'Internal server error' }, 500);
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteComment = async (c: Context) => {
|
// Create a new comment
|
||||||
|
export const createComment = async (c: Context) => {
|
||||||
try {
|
try {
|
||||||
const { comment_id } = c.req.param();
|
const { post_id, content } = await c.req.json();
|
||||||
const user_id = c.get('user')?.id;
|
const user_id = c.get('user')?.id;
|
||||||
|
|
||||||
if (!user_id) {
|
// Validate required fields
|
||||||
return c.json({ error: 'Unauthorized' }, 401);
|
if (!post_id || !content) {
|
||||||
|
return c.json({ error: 'post_id and content are required' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the comment belongs to the user
|
// Check if post exists
|
||||||
const { data: comment, error: fetchError } = await supabase
|
const { data: post, error: postError } = await supabase
|
||||||
|
.from('posts')
|
||||||
|
.select('post_id')
|
||||||
|
.eq('post_id', post_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (postError || !post) {
|
||||||
|
return c.json({ error: 'Post not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: comment, error } = await supabase
|
||||||
.from('comments')
|
.from('comments')
|
||||||
|
.insert({
|
||||||
|
post_id,
|
||||||
|
user_id,
|
||||||
|
content
|
||||||
|
})
|
||||||
.select()
|
.select()
|
||||||
.eq('comment_id', comment_id)
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error creating comment:', error);
|
||||||
|
return c.json({ error: 'Failed to create comment' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(comment, 201);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in createComment:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update an existing comment
|
||||||
|
export const updateComment = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const commentId = c.req.param('comment_id');
|
||||||
|
const { content } = await c.req.json();
|
||||||
|
const user_id = c.get('user')?.id;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!content) {
|
||||||
|
return c.json({ error: 'content is required' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if comment exists and belongs to the user
|
||||||
|
const { data: existingComment, error: existingCommentError } = await supabase
|
||||||
|
.from('comments')
|
||||||
|
.select('comment_id')
|
||||||
|
.eq('comment_id', commentId)
|
||||||
.eq('user_id', user_id)
|
.eq('user_id', user_id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (fetchError || !comment) {
|
if (existingCommentError || !existingComment) {
|
||||||
return c.json({ error: 'Comment not found or unauthorized' }, 404);
|
return c.json({ error: 'Comment not found or unauthorized' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { data: comment, error } = await supabase
|
||||||
|
.from('comments')
|
||||||
|
.update({ content })
|
||||||
|
.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(comment);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in updateComment:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { error: deleteError } = await supabase
|
// Delete a comment
|
||||||
|
export const deleteComment = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const commentId = c.req.param('comment_id');
|
||||||
|
const user_id = c.get('user')?.id;
|
||||||
|
|
||||||
|
// Check if comment exists and belongs to the user
|
||||||
|
const { data: existingComment, error: existingCommentError } = await supabase
|
||||||
|
.from('comments')
|
||||||
|
.select('comment_id')
|
||||||
|
.eq('comment_id', commentId)
|
||||||
|
.eq('user_id', user_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (existingCommentError || !existingComment) {
|
||||||
|
return c.json({ error: 'Comment not found or unauthorized' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await supabase
|
||||||
.from('comments')
|
.from('comments')
|
||||||
.delete()
|
.delete()
|
||||||
.eq('comment_id', comment_id);
|
.eq('comment_id', commentId);
|
||||||
|
|
||||||
if (deleteError) {
|
if (error) {
|
||||||
return c.json({ error: deleteError.message }, 500);
|
console.error('Error deleting comment:', error);
|
||||||
|
return c.json({ error: 'Failed to delete comment' }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.body(null, 204);
|
return c.body(null, 204);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error in deleteComment:', error);
|
||||||
return c.json({ error: 'Internal server error' }, 500);
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Context } from 'hono';
|
import { Context } from 'hono';
|
||||||
import supabase from '../utils/supabase';
|
import supabase from '../utils/supabase';
|
||||||
|
|
||||||
export const getInfluencers = async (c: Context) => {
|
// Get all influencers with filtering and pagination
|
||||||
|
const getInfluencers = async (c: Context) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
platform,
|
platform,
|
||||||
@@ -20,12 +21,13 @@ export const getInfluencers = async (c: Context) => {
|
|||||||
name,
|
name,
|
||||||
platform,
|
platform,
|
||||||
profile_url,
|
profile_url,
|
||||||
|
external_id,
|
||||||
followers_count,
|
followers_count,
|
||||||
video_count,
|
video_count,
|
||||||
platform_count,
|
platform_count,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
`);
|
`, { count: 'exact' });
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
if (platform) {
|
if (platform) {
|
||||||
@@ -49,6 +51,7 @@ export const getInfluencers = async (c: Context) => {
|
|||||||
const { data: influencers, error, count } = await query;
|
const { data: influencers, error, count } = await query;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
console.error('Error fetching influencers:', error);
|
||||||
return c.json({ error: error.message }, 500);
|
return c.json({ error: error.message }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,11 +62,13 @@ export const getInfluencers = async (c: Context) => {
|
|||||||
offset: Number(offset)
|
offset: Number(offset)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error in getInfluencers:', error);
|
||||||
return c.json({ error: 'Internal server error' }, 500);
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getInfluencerById = async (c: Context) => {
|
// Get a specific influencer by ID
|
||||||
|
const getInfluencerById = async (c: Context) => {
|
||||||
try {
|
try {
|
||||||
const { influencer_id } = c.req.param();
|
const { influencer_id } = c.req.param();
|
||||||
|
|
||||||
@@ -74,32 +79,30 @@ export const getInfluencerById = async (c: Context) => {
|
|||||||
name,
|
name,
|
||||||
platform,
|
platform,
|
||||||
profile_url,
|
profile_url,
|
||||||
|
external_id,
|
||||||
followers_count,
|
followers_count,
|
||||||
video_count,
|
video_count,
|
||||||
platform_count,
|
platform_count,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at
|
||||||
posts (
|
|
||||||
post_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
published_at
|
|
||||||
)
|
|
||||||
`)
|
`)
|
||||||
.eq('influencer_id', influencer_id)
|
.eq('influencer_id', influencer_id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
console.error('Error fetching influencer:', error);
|
||||||
return c.json({ error: 'Influencer not found' }, 404);
|
return c.json({ error: 'Influencer not found' }, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json(influencer);
|
return c.json(influencer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error in getInfluencerById:', error);
|
||||||
return c.json({ error: 'Internal server error' }, 500);
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getInfluencerStats = async (c: Context) => {
|
// Get aggregated stats for influencers
|
||||||
|
const getInfluencerStats = async (c: Context) => {
|
||||||
try {
|
try {
|
||||||
const { platform } = c.req.query();
|
const { platform } = c.req.query();
|
||||||
|
|
||||||
@@ -114,6 +117,7 @@ export const getInfluencerStats = async (c: Context) => {
|
|||||||
const { data: stats, error } = await query;
|
const { data: stats, error } = await query;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
console.error('Error fetching influencer stats:', error);
|
||||||
return c.json({ error: error.message }, 500);
|
return c.json({ error: error.message }, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,11 +130,212 @@ export const getInfluencerStats = async (c: Context) => {
|
|||||||
),
|
),
|
||||||
average_videos: Math.round(
|
average_videos: Math.round(
|
||||||
stats.reduce((sum: number, item: any) => sum + (item.video_count || 0), 0) / (stats.length || 1)
|
stats.reduce((sum: number, item: any) => sum + (item.video_count || 0), 0) / (stats.length || 1)
|
||||||
)
|
),
|
||||||
|
platform_distribution: stats.reduce((acc: Record<string, number>, item: any) => {
|
||||||
|
const platform = item.platform || 'unknown';
|
||||||
|
acc[platform] = (acc[platform] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
};
|
};
|
||||||
|
|
||||||
return c.json(aggregatedStats);
|
return c.json(aggregatedStats);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error in getInfluencerStats:', error);
|
||||||
return c.json({ error: 'Internal server error' }, 500);
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a new influencer
|
||||||
|
const createInfluencer = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const { name, platform, profile_url, external_id, followers_count, video_count } = await c.req.json();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return c.json({ error: 'Name is required' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: influencer, error } = await supabase
|
||||||
|
.from('influencers')
|
||||||
|
.insert({
|
||||||
|
name,
|
||||||
|
platform,
|
||||||
|
profile_url,
|
||||||
|
external_id,
|
||||||
|
followers_count: followers_count || 0,
|
||||||
|
video_count: video_count || 0
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (error.code === '23505') { // Unique constraint violation
|
||||||
|
return c.json({ error: 'An influencer with this external ID already exists' }, 409);
|
||||||
|
}
|
||||||
|
console.error('Error creating influencer:', error);
|
||||||
|
return c.json({ error: 'Failed to create influencer' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(influencer, 201);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in createInfluencer:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update an existing influencer
|
||||||
|
const updateInfluencer = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const { influencer_id } = c.req.param();
|
||||||
|
const { name, platform, profile_url, external_id, followers_count, video_count } = await c.req.json();
|
||||||
|
|
||||||
|
// Check if influencer exists
|
||||||
|
const { data: existingInfluencer, error: fetchError } = await supabase
|
||||||
|
.from('influencers')
|
||||||
|
.select('influencer_id')
|
||||||
|
.eq('influencer_id', influencer_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (fetchError || !existingInfluencer) {
|
||||||
|
return c.json({ error: 'Influencer not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData: any = {};
|
||||||
|
if (name !== undefined) updateData.name = name;
|
||||||
|
if (platform !== undefined) updateData.platform = platform;
|
||||||
|
if (profile_url !== undefined) updateData.profile_url = profile_url;
|
||||||
|
if (external_id !== undefined) updateData.external_id = external_id;
|
||||||
|
if (followers_count !== undefined) updateData.followers_count = followers_count;
|
||||||
|
if (video_count !== undefined) updateData.video_count = video_count;
|
||||||
|
updateData.updated_at = new Date().toISOString();
|
||||||
|
|
||||||
|
const { data: updatedInfluencer, error: updateError } = await supabase
|
||||||
|
.from('influencers')
|
||||||
|
.update(updateData)
|
||||||
|
.eq('influencer_id', influencer_id)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (updateError) {
|
||||||
|
if (updateError.code === '23505') { // Unique constraint violation
|
||||||
|
return c.json({ error: 'An influencer with this external ID already exists' }, 409);
|
||||||
|
}
|
||||||
|
console.error('Error updating influencer:', updateError);
|
||||||
|
return c.json({ error: 'Failed to update influencer' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(updatedInfluencer);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in updateInfluencer:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete an influencer
|
||||||
|
const deleteInfluencer = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const { influencer_id } = c.req.param();
|
||||||
|
|
||||||
|
// Check if influencer exists
|
||||||
|
const { data: existingInfluencer, error: fetchError } = await supabase
|
||||||
|
.from('influencers')
|
||||||
|
.select('influencer_id')
|
||||||
|
.eq('influencer_id', influencer_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (fetchError || !existingInfluencer) {
|
||||||
|
return c.json({ error: 'Influencer not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if influencer has any posts
|
||||||
|
const { data: posts, error: postsError } = await supabase
|
||||||
|
.from('posts')
|
||||||
|
.select('post_id')
|
||||||
|
.eq('influencer_id', influencer_id)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!postsError && posts && posts.length > 0) {
|
||||||
|
return c.json({
|
||||||
|
error: 'Cannot delete influencer with existing posts. Delete their posts first or use the force parameter.'
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the influencer
|
||||||
|
const { error: deleteError } = await supabase
|
||||||
|
.from('influencers')
|
||||||
|
.delete()
|
||||||
|
.eq('influencer_id', influencer_id);
|
||||||
|
|
||||||
|
if (deleteError) {
|
||||||
|
console.error('Error deleting influencer:', deleteError);
|
||||||
|
return c.json({ error: 'Failed to delete influencer' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.body(null, 204);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in deleteInfluencer:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all posts for a specific influencer
|
||||||
|
const getInfluencerPosts = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const { influencer_id } = c.req.param();
|
||||||
|
const { limit = '20', offset = '0' } = c.req.query();
|
||||||
|
|
||||||
|
// Check if influencer exists
|
||||||
|
const { data: influencer, error: influencerError } = await supabase
|
||||||
|
.from('influencers')
|
||||||
|
.select('influencer_id, name')
|
||||||
|
.eq('influencer_id', influencer_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (influencerError || !influencer) {
|
||||||
|
return c.json({ error: 'Influencer not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get posts for the influencer
|
||||||
|
const { data: posts, error: postsError, count } = await supabase
|
||||||
|
.from('posts')
|
||||||
|
.select(`
|
||||||
|
post_id,
|
||||||
|
platform,
|
||||||
|
post_url,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
published_at,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
`, { count: 'exact' })
|
||||||
|
.eq('influencer_id', influencer_id)
|
||||||
|
.order('published_at', { ascending: false })
|
||||||
|
.range(parseInt(offset), parseInt(offset) + parseInt(limit) - 1);
|
||||||
|
|
||||||
|
if (postsError) {
|
||||||
|
console.error('Error fetching influencer posts:', postsError);
|
||||||
|
return c.json({ error: 'Failed to fetch influencer posts' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
influencer_id,
|
||||||
|
influencer_name: influencer.name,
|
||||||
|
posts,
|
||||||
|
count,
|
||||||
|
limit: parseInt(limit),
|
||||||
|
offset: parseInt(offset)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getInfluencerPosts:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getInfluencers,
|
||||||
|
getInfluencerById,
|
||||||
|
getInfluencerStats,
|
||||||
|
createInfluencer,
|
||||||
|
updateInfluencer,
|
||||||
|
deleteInfluencer,
|
||||||
|
getInfluencerPosts
|
||||||
};
|
};
|
||||||
303
backend/src/controllers/postsController.ts
Normal file
303
backend/src/controllers/postsController.ts
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
import { Context } from 'hono';
|
||||||
|
import supabase from '../utils/supabase';
|
||||||
|
|
||||||
|
// Get all posts with filtering and pagination
|
||||||
|
const getPosts = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
influencer_id,
|
||||||
|
platform,
|
||||||
|
limit = '10',
|
||||||
|
offset = '0',
|
||||||
|
sort_by = 'published_at',
|
||||||
|
sort_order = 'desc'
|
||||||
|
} = c.req.query();
|
||||||
|
|
||||||
|
let query = supabase
|
||||||
|
.from('posts')
|
||||||
|
.select(`
|
||||||
|
post_id,
|
||||||
|
influencer_id,
|
||||||
|
platform,
|
||||||
|
post_url,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
published_at,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
influencers (
|
||||||
|
name,
|
||||||
|
profile_url,
|
||||||
|
followers_count
|
||||||
|
)
|
||||||
|
`, { count: 'exact' });
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (influencer_id) {
|
||||||
|
query = query.eq('influencer_id', influencer_id);
|
||||||
|
}
|
||||||
|
if (platform) {
|
||||||
|
query = query.eq('platform', platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
if (sort_by && ['published_at', '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: posts, error, count } = await query;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error fetching posts:', error);
|
||||||
|
return c.json({ error: 'Failed to fetch posts' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
posts,
|
||||||
|
count,
|
||||||
|
limit: Number(limit),
|
||||||
|
offset: Number(offset)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getPosts:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get a specific post by ID
|
||||||
|
const getPostById = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const postId = c.req.param('post_id');
|
||||||
|
|
||||||
|
const { data: post, error } = await supabase
|
||||||
|
.from('posts')
|
||||||
|
.select(`
|
||||||
|
post_id,
|
||||||
|
influencer_id,
|
||||||
|
platform,
|
||||||
|
post_url,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
published_at,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
influencers (
|
||||||
|
name,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(post);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getPostById:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a new post
|
||||||
|
const createPost = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const { influencer_id, platform, post_url, title, description, published_at } = await c.req.json();
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!influencer_id || !post_url) {
|
||||||
|
return c.json({ error: 'influencer_id and post_url are required' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if influencer exists
|
||||||
|
const { data: influencer, error: influencerError } = await supabase
|
||||||
|
.from('influencers')
|
||||||
|
.select('influencer_id')
|
||||||
|
.eq('influencer_id', influencer_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (influencerError || !influencer) {
|
||||||
|
return c.json({ error: 'Influencer not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if post URL already exists
|
||||||
|
const { data: existingPost, error: existingPostError } = await supabase
|
||||||
|
.from('posts')
|
||||||
|
.select('post_id')
|
||||||
|
.eq('post_url', post_url)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (existingPost) {
|
||||||
|
return c.json({ error: 'Post with this URL already exists' }, 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: post, error } = await supabase
|
||||||
|
.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(post, 201);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in createPost:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update an existing post
|
||||||
|
const updatePost = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const postId = c.req.param('post_id');
|
||||||
|
const { platform, post_url, title, description, published_at } = await c.req.json();
|
||||||
|
|
||||||
|
// Check if post exists
|
||||||
|
const { data: existingPost, error: existingPostError } = await supabase
|
||||||
|
.from('posts')
|
||||||
|
.select('post_id')
|
||||||
|
.eq('post_id', postId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (existingPostError || !existingPost) {
|
||||||
|
return c.json({ error: 'Post not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if post URL already exists (if updating URL)
|
||||||
|
if (post_url) {
|
||||||
|
const { data: duplicatePost, error: duplicatePostError } = await supabase
|
||||||
|
.from('posts')
|
||||||
|
.select('post_id')
|
||||||
|
.eq('post_url', post_url)
|
||||||
|
.neq('post_id', postId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (duplicatePost) {
|
||||||
|
return c.json({ error: 'Another post with this URL already exists' }, 409);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: post, error } = await supabase
|
||||||
|
.from('posts')
|
||||||
|
.update({
|
||||||
|
platform,
|
||||||
|
post_url,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
published_at
|
||||||
|
})
|
||||||
|
.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(post);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in updatePost:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete a post
|
||||||
|
const deletePost = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const postId = c.req.param('post_id');
|
||||||
|
|
||||||
|
// Check if post exists
|
||||||
|
const { data: existingPost, error: existingPostError } = await supabase
|
||||||
|
.from('posts')
|
||||||
|
.select('post_id')
|
||||||
|
.eq('post_id', postId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (existingPostError || !existingPost) {
|
||||||
|
return c.json({ error: 'Post not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete post
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('posts')
|
||||||
|
.delete()
|
||||||
|
.eq('post_id', postId);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error deleting post:', error);
|
||||||
|
return c.json({ error: 'Failed to delete post' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.body(null, 204);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in deletePost:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get comments for a specific post
|
||||||
|
const getPostComments = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const postId = c.req.param('post_id');
|
||||||
|
const { limit = '10', offset = '0' } = c.req.query();
|
||||||
|
|
||||||
|
const { data: comments, error, count } = await supabase
|
||||||
|
.from('comments')
|
||||||
|
.select(`
|
||||||
|
comment_id,
|
||||||
|
content,
|
||||||
|
sentiment_score,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
user_id
|
||||||
|
`, { count: 'exact' })
|
||||||
|
.eq('post_id', postId)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.range(Number(offset), Number(offset) + Number(limit) - 1);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error fetching post comments:', error);
|
||||||
|
return c.json({ error: 'Failed to fetch comments' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
comments,
|
||||||
|
count,
|
||||||
|
limit: Number(limit),
|
||||||
|
offset: Number(offset)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getPostComments:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getPosts,
|
||||||
|
getPostById,
|
||||||
|
createPost,
|
||||||
|
updatePost,
|
||||||
|
deletePost,
|
||||||
|
getPostComments
|
||||||
|
};
|
||||||
340
backend/src/controllers/projectsController.ts
Normal file
340
backend/src/controllers/projectsController.ts
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
import { Context } from 'hono';
|
||||||
|
import supabase from '../utils/supabase';
|
||||||
|
|
||||||
|
// Get all projects with optional filtering
|
||||||
|
const getProjects = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const { limit = '20', offset = '0', created_by } = c.req.query();
|
||||||
|
|
||||||
|
let query = supabase
|
||||||
|
.from('projects')
|
||||||
|
.select('id, name, description, created_by, created_at, updated_at', { count: 'exact' });
|
||||||
|
|
||||||
|
// Apply filtering if user ID is provided
|
||||||
|
if (created_by) {
|
||||||
|
query = query.eq('created_by', created_by);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply pagination and ordering
|
||||||
|
query = query
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
.range(parseInt(offset), parseInt(offset) + parseInt(limit) - 1);
|
||||||
|
|
||||||
|
const { data: projects, error, count } = await query;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error fetching projects:', error);
|
||||||
|
return c.json({ error: 'Failed to fetch projects' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
projects,
|
||||||
|
count,
|
||||||
|
limit: parseInt(limit),
|
||||||
|
offset: parseInt(offset)
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getProjects:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get a specific project by ID
|
||||||
|
const getProjectById = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const projectId = c.req.param('id');
|
||||||
|
|
||||||
|
const { data: project, error } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.select('id, name, description, created_by, created_at, updated_at')
|
||||||
|
.eq('id', projectId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error fetching project:', error);
|
||||||
|
return c.json({ error: 'Project not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(project);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getProjectById:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a new project
|
||||||
|
const createProject = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const { name, description } = await c.req.json();
|
||||||
|
const user = c.get('user');
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return c.json({ error: 'Project name is required' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: project, error } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.insert({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
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(project, 201);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in createProject:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update an existing project
|
||||||
|
const updateProject = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const projectId = c.req.param('id');
|
||||||
|
const { name, description } = await c.req.json();
|
||||||
|
const user = c.get('user');
|
||||||
|
|
||||||
|
// Check if project exists and user has permission
|
||||||
|
const { data: existingProject, error: fetchError } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.select('id, created_by')
|
||||||
|
.eq('id', projectId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (fetchError || !existingProject) {
|
||||||
|
return c.json({ error: 'Project not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingProject.created_by !== user.id) {
|
||||||
|
return c.json({ error: 'You do not have permission to update this project' }, 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the project
|
||||||
|
const { data: updatedProject, error: updateError } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.update({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
.eq('id', projectId)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (updateError) {
|
||||||
|
console.error('Error updating project:', updateError);
|
||||||
|
return c.json({ error: 'Failed to update project' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(updatedProject);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in updateProject:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete a project
|
||||||
|
const deleteProject = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const projectId = c.req.param('id');
|
||||||
|
const user = c.get('user');
|
||||||
|
|
||||||
|
// Check if project exists and user has permission
|
||||||
|
const { data: existingProject, error: fetchError } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.select('id, created_by')
|
||||||
|
.eq('id', projectId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (fetchError || !existingProject) {
|
||||||
|
return c.json({ error: 'Project not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingProject.created_by !== user.id) {
|
||||||
|
return c.json({ error: 'You do not have permission to delete this project' }, 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the project
|
||||||
|
const { error: deleteError } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.delete()
|
||||||
|
.eq('id', projectId);
|
||||||
|
|
||||||
|
if (deleteError) {
|
||||||
|
console.error('Error deleting project:', deleteError);
|
||||||
|
return c.json({ error: 'Failed to delete project' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.body(null, 204);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in deleteProject:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all influencers associated with a project
|
||||||
|
const getProjectInfluencers = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const projectId = c.req.param('id');
|
||||||
|
|
||||||
|
// First check if the project exists
|
||||||
|
const { data: project, error: projectError } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.select('id')
|
||||||
|
.eq('id', projectId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (projectError) {
|
||||||
|
return c.json({ error: 'Project not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the influencers associated with the project
|
||||||
|
const { data: projectInfluencers, error } = await supabase
|
||||||
|
.from('project_influencers')
|
||||||
|
.select(`
|
||||||
|
id,
|
||||||
|
influencer_id,
|
||||||
|
created_at,
|
||||||
|
influencers:influencer_id (
|
||||||
|
influencer_id,
|
||||||
|
name,
|
||||||
|
platform,
|
||||||
|
profile_url,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(projectInfluencers);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in getProjectInfluencers:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add an influencer to a project
|
||||||
|
const addInfluencerToProject = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const projectId = c.req.param('id');
|
||||||
|
const { influencer_id } = await c.req.json();
|
||||||
|
const user = c.get('user');
|
||||||
|
|
||||||
|
if (!influencer_id) {
|
||||||
|
return c.json({ error: 'Influencer ID is required' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the project exists and the user has permission
|
||||||
|
const { data: project, error: projectError } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.select('id, created_by')
|
||||||
|
.eq('id', projectId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (projectError || !project) {
|
||||||
|
return c.json({ error: 'Project not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.created_by !== user.id) {
|
||||||
|
return c.json({ error: 'You do not have permission to modify this project' }, 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the influencer exists
|
||||||
|
const { data: influencer, error: influencerError } = await supabase
|
||||||
|
.from('influencers')
|
||||||
|
.select('influencer_id')
|
||||||
|
.eq('influencer_id', influencer_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (influencerError || !influencer) {
|
||||||
|
return c.json({ error: 'Influencer not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the influencer to the project
|
||||||
|
const { data: projectInfluencer, error } = await supabase
|
||||||
|
.from('project_influencers')
|
||||||
|
.insert({
|
||||||
|
project_id: projectId,
|
||||||
|
influencer_id
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (error.code === '23505') { // Unique constraint violation
|
||||||
|
return c.json({ error: 'Influencer is already in this project' }, 409);
|
||||||
|
}
|
||||||
|
console.error('Error adding influencer to project:', error);
|
||||||
|
return c.json({ error: 'Failed to add influencer to project' }, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(projectInfluencer, 201);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in addInfluencerToProject:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove an influencer from a project
|
||||||
|
const removeInfluencerFromProject = async (c: Context) => {
|
||||||
|
try {
|
||||||
|
const projectId = c.req.param('id');
|
||||||
|
const influencerId = c.req.param('influencer_id');
|
||||||
|
const user = c.get('user');
|
||||||
|
|
||||||
|
// Check if the project exists and the user has permission
|
||||||
|
const { data: project, error: projectError } = await supabase
|
||||||
|
.from('projects')
|
||||||
|
.select('id, created_by')
|
||||||
|
.eq('id', projectId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (projectError || !project) {
|
||||||
|
return c.json({ error: 'Project not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.created_by !== user.id) {
|
||||||
|
return c.json({ error: 'You do not have permission to modify this project' }, 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the influencer from the project
|
||||||
|
const { error } = await supabase
|
||||||
|
.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.body(null, 204);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in removeInfluencerFromProject:', error);
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getProjects,
|
||||||
|
getProjectById,
|
||||||
|
createProject,
|
||||||
|
updateProject,
|
||||||
|
deleteProject,
|
||||||
|
getProjectInfluencers,
|
||||||
|
addInfluencerToProject,
|
||||||
|
removeInfluencerFromProject
|
||||||
|
};
|
||||||
@@ -10,10 +10,11 @@ import postsRouter from './routes/posts';
|
|||||||
import projectCommentsRouter from './routes/projectComments';
|
import projectCommentsRouter from './routes/projectComments';
|
||||||
import commentsRouter from './routes/comments';
|
import commentsRouter from './routes/comments';
|
||||||
import influencersRouter from './routes/influencers';
|
import influencersRouter from './routes/influencers';
|
||||||
|
import projectsRouter from './routes/projects';
|
||||||
import { connectRedis } from './utils/redis';
|
import { connectRedis } from './utils/redis';
|
||||||
import { initClickHouse } from './utils/clickhouse';
|
import { initClickHouse } from './utils/clickhouse';
|
||||||
import { initWorkers } from './utils/queue';
|
import { initWorkers } from './utils/queue';
|
||||||
import { initDatabase, createSampleData, checkDatabaseConnection } from './utils/initDatabase';
|
import { checkDatabaseConnection } from './utils/initDatabase';
|
||||||
import { createSwaggerUI } from './swagger';
|
import { createSwaggerUI } from './swagger';
|
||||||
import { initScheduledTaskWorkers } from './utils/scheduledTasks';
|
import { initScheduledTaskWorkers } from './utils/scheduledTasks';
|
||||||
|
|
||||||
@@ -39,42 +40,6 @@ app.get('/', (c) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 数据库初始化路由
|
|
||||||
app.post('/api/admin/init-db', async (c) => {
|
|
||||||
try {
|
|
||||||
const result = await 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 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
|
// Routes
|
||||||
app.route('/api/auth', authRouter);
|
app.route('/api/auth', authRouter);
|
||||||
app.route('/api/analytics', analyticsRouter);
|
app.route('/api/analytics', analyticsRouter);
|
||||||
@@ -83,6 +48,7 @@ app.route('/api/posts', postsRouter);
|
|||||||
app.route('/api/project-comments', projectCommentsRouter);
|
app.route('/api/project-comments', projectCommentsRouter);
|
||||||
app.route('/api/comments', commentsRouter);
|
app.route('/api/comments', commentsRouter);
|
||||||
app.route('/api/influencers', influencersRouter);
|
app.route('/api/influencers', influencersRouter);
|
||||||
|
app.route('/api/projects', projectsRouter);
|
||||||
|
|
||||||
// Swagger UI
|
// Swagger UI
|
||||||
const swaggerApp = createSwaggerUI();
|
const swaggerApp = createSwaggerUI();
|
||||||
@@ -117,9 +83,6 @@ const startServer = async () => {
|
|||||||
console.log('Some features may not work correctly if database is not properly set up');
|
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 workers for background processing
|
// Initialize workers for background processing
|
||||||
console.log('🏗️ Initializing workers...');
|
console.log('🏗️ Initializing workers...');
|
||||||
const workers = {
|
const workers = {
|
||||||
@@ -138,8 +101,6 @@ const startServer = async () => {
|
|||||||
|
|
||||||
console.log(`Server running at http://localhost:${port}`);
|
console.log(`Server running at http://localhost:${port}`);
|
||||||
console.log(`Swagger UI available at http://localhost:${port}/swagger`);
|
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
|
// Handle graceful shutdown
|
||||||
const shutdown = async () => {
|
const shutdown = async () => {
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { getComments, createComment, deleteComment } from '../controllers/commentsController';
|
|
||||||
import { authMiddleware } from '../middlewares/auth';
|
import { authMiddleware } from '../middlewares/auth';
|
||||||
|
import { getComments, getCommentById, createComment, updateComment, deleteComment } from '../controllers/commentsController';
|
||||||
|
|
||||||
const commentsRouter = new Hono();
|
const commentsRouter = new Hono();
|
||||||
|
|
||||||
// Public routes
|
// Public routes
|
||||||
commentsRouter.get('/', getComments);
|
commentsRouter.get('/', getComments);
|
||||||
|
commentsRouter.get('/:comment_id', getCommentById);
|
||||||
|
|
||||||
// Protected routes
|
// Protected routes
|
||||||
commentsRouter.post('/', authMiddleware, createComment);
|
commentsRouter.post('/', authMiddleware, createComment);
|
||||||
|
commentsRouter.put('/:comment_id', authMiddleware, updateComment);
|
||||||
commentsRouter.delete('/:comment_id', authMiddleware, deleteComment);
|
commentsRouter.delete('/:comment_id', authMiddleware, deleteComment);
|
||||||
|
|
||||||
export default commentsRouter;
|
export default commentsRouter;
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { getInfluencers, getInfluencerById, getInfluencerStats } from '../controllers/influencersController';
|
import { authMiddleware } from '../middlewares/auth';
|
||||||
|
import influencersController from '../controllers/influencersController';
|
||||||
|
|
||||||
const influencersRouter = new Hono();
|
const influencersRouter = new Hono();
|
||||||
|
|
||||||
// Public routes
|
// Public routes
|
||||||
influencersRouter.get('/', getInfluencers);
|
influencersRouter.get('/', influencersController.getInfluencers);
|
||||||
influencersRouter.get('/stats', getInfluencerStats);
|
influencersRouter.get('/stats', influencersController.getInfluencerStats);
|
||||||
influencersRouter.get('/:influencer_id', getInfluencerById);
|
influencersRouter.get('/:influencer_id', influencersController.getInfluencerById);
|
||||||
|
|
||||||
|
// Protected routes
|
||||||
|
influencersRouter.post('/', authMiddleware, influencersController.createInfluencer);
|
||||||
|
influencersRouter.put('/:influencer_id', authMiddleware, influencersController.updateInfluencer);
|
||||||
|
influencersRouter.delete('/:influencer_id', authMiddleware, influencersController.deleteInfluencer);
|
||||||
|
|
||||||
|
// Related data
|
||||||
|
influencersRouter.get('/:influencer_id/posts', influencersController.getInfluencerPosts);
|
||||||
|
|
||||||
export default influencersRouter;
|
export default influencersRouter;
|
||||||
@@ -1,686 +1,17 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { authMiddleware } from '../middlewares/auth';
|
import { authMiddleware } from '../middlewares/auth';
|
||||||
import supabase from '../utils/supabase';
|
import postsController from '../controllers/postsController';
|
||||||
import clickhouse from '../utils/clickhouse';
|
|
||||||
import { getRedisClient } from '../utils/redis';
|
|
||||||
|
|
||||||
// Define user type
|
|
||||||
interface User {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define stats type
|
|
||||||
interface PostStats {
|
|
||||||
post_id: string;
|
|
||||||
views: number | null;
|
|
||||||
likes: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extend Hono's Context type
|
|
||||||
declare module 'hono' {
|
|
||||||
interface ContextVariableMap {
|
|
||||||
user: User;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const postsRouter = new Hono();
|
const postsRouter = new Hono();
|
||||||
|
|
||||||
// Apply auth middleware to most routes
|
// Public routes
|
||||||
postsRouter.use('*', authMiddleware);
|
postsRouter.get('/', postsController.getPosts);
|
||||||
|
postsRouter.get('/:post_id', postsController.getPostById);
|
||||||
|
postsRouter.get('/:post_id/comments', postsController.getPostComments);
|
||||||
|
|
||||||
// 创建新帖子
|
// Protected routes
|
||||||
postsRouter.post('/', async (c) => {
|
postsRouter.post('/', authMiddleware, postsController.createPost);
|
||||||
try {
|
postsRouter.put('/:post_id', authMiddleware, postsController.updatePost);
|
||||||
const {
|
postsRouter.delete('/:post_id', authMiddleware, postsController.deletePost);
|
||||||
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
|
|
||||||
.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
|
|
||||||
.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.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 getRedisClient();
|
|
||||||
const cachedStats: PostStats[] = 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.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.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
|
|
||||||
.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 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.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.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.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
|
|
||||||
.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
|
|
||||||
.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
|
|
||||||
.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
|
|
||||||
.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 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
|
|
||||||
.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
|
|
||||||
.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
|
|
||||||
.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
|
|
||||||
.from('posts')
|
|
||||||
.select('influencer_id, platform')
|
|
||||||
.eq('post_id', postId)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (post) {
|
|
||||||
await clickhouse.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
|
|
||||||
.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
|
|
||||||
.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
|
|
||||||
.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
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default postsRouter;
|
export default postsRouter;
|
||||||
21
backend/src/routes/projects.ts
Normal file
21
backend/src/routes/projects.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Hono } from 'hono';
|
||||||
|
import { authMiddleware } from '../middlewares/auth';
|
||||||
|
import projectsController from '../controllers/projectsController';
|
||||||
|
|
||||||
|
const projectsRouter = new Hono();
|
||||||
|
|
||||||
|
// Public routes
|
||||||
|
projectsRouter.get('/', projectsController.getProjects);
|
||||||
|
projectsRouter.get('/:id', projectsController.getProjectById);
|
||||||
|
|
||||||
|
// Protected routes - require authentication
|
||||||
|
projectsRouter.post('/', authMiddleware, projectsController.createProject);
|
||||||
|
projectsRouter.put('/:id', authMiddleware, projectsController.updateProject);
|
||||||
|
projectsRouter.delete('/:id', authMiddleware, projectsController.deleteProject);
|
||||||
|
|
||||||
|
// Project influencers management
|
||||||
|
projectsRouter.get('/:id/influencers', projectsController.getProjectInfluencers);
|
||||||
|
projectsRouter.post('/:id/influencers', authMiddleware, projectsController.addInfluencerToProject);
|
||||||
|
projectsRouter.delete('/:id/influencers/:influencer_id', authMiddleware, projectsController.removeInfluencerFromProject);
|
||||||
|
|
||||||
|
export default projectsRouter;
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user