cleaning up api

This commit is contained in:
2025-03-12 15:49:47 +08:00
parent 285a0c780a
commit 9d7d1abb49
10 changed files with 1974 additions and 839 deletions

View File

@@ -1,27 +1,43 @@
import { Context } from 'hono';
import supabase from '../utils/supabase';
// Get all comments with filtering and pagination
export const getComments = async (c: Context) => {
try {
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) {
// 获取特定帖子的评论
query = supabase.rpc('get_comments_for_post', { post_id_param: post_id });
} else {
// 获取所有评论
query = supabase.rpc('get_comments_with_posts');
query = query.eq('post_id', post_id);
}
// 应用分页
query = query.range(Number(offset), Number(offset) + Number(limit) - 1);
// Apply pagination and sorting
query = query
.order('created_at', { ascending: false })
.range(Number(offset), Number(offset) + Number(limit) - 1);
const { data: comments, error, count } = await query;
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({
@@ -31,90 +47,169 @@ export const getComments = async (c: Context) => {
offset: Number(offset)
});
} catch (error) {
console.error('Error in getComments:', error);
return c.json({ error: 'Internal server error' }, 500);
}
};
// Get a specific comment by ID
export const getCommentById = async (c: Context) => {
try {
const commentId = c.req.param('comment_id');
const { data: comment, error } = await supabase
.from('comments')
.select(`
comment_id,
post_id,
user_id,
content,
sentiment_score,
created_at,
updated_at,
posts (
post_id,
title,
platform
)
`)
.eq('comment_id', commentId)
.single();
if (error) {
console.error('Error fetching comment:', error);
return c.json({ error: 'Failed to fetch comment' }, 500);
}
if (!comment) {
return c.json({ error: 'Comment not found' }, 404);
}
return c.json(comment);
} catch (error) {
console.error('Error in getCommentById:', error);
return c.json({ error: 'Internal server error' }, 500);
}
};
// Create a new comment
export const createComment = async (c: Context) => {
try {
const { post_id, content } = await c.req.json();
const user_id = c.get('user')?.id;
if (!user_id) {
return c.json({ error: 'Unauthorized' }, 401);
// Validate required fields
if (!post_id || !content) {
return c.json({ error: 'post_id and content are required' }, 400);
}
// Check if post exists
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')
.insert({
post_id,
content,
user_id
user_id,
content
})
.select(`
comment_id,
content,
sentiment_score,
created_at,
updated_at,
post_id,
user_id
`)
.select()
.single();
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();
if (!userError && userProfile) {
comment.user_profile = userProfile;
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);
}
};
export const deleteComment = async (c: Context) => {
// Update an existing comment
export const updateComment = async (c: Context) => {
try {
const { comment_id } = c.req.param();
const commentId = c.req.param('comment_id');
const { content } = await c.req.json();
const user_id = c.get('user')?.id;
if (!user_id) {
return c.json({ error: 'Unauthorized' }, 401);
// Validate required fields
if (!content) {
return c.json({ error: 'content is required' }, 400);
}
// Check if the comment belongs to the user
const { data: comment, error: fetchError } = await supabase
// Check if comment exists and belongs to the user
const { data: existingComment, error: existingCommentError } = await supabase
.from('comments')
.select()
.eq('comment_id', comment_id)
.select('comment_id')
.eq('comment_id', commentId)
.eq('user_id', user_id)
.single();
if (fetchError || !comment) {
if (existingCommentError || !existingComment) {
return c.json({ error: 'Comment not found or unauthorized' }, 404);
}
const { error: deleteError } = await supabase
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);
}
};
// 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')
.delete()
.eq('comment_id', comment_id);
.eq('comment_id', commentId);
if (deleteError) {
return c.json({ error: deleteError.message }, 500);
if (error) {
console.error('Error deleting comment:', error);
return c.json({ error: 'Failed to delete comment' }, 500);
}
return c.body(null, 204);
} catch (error) {
console.error('Error in deleteComment:', error);
return c.json({ error: 'Internal server error' }, 500);
}
};

View File

@@ -1,7 +1,8 @@
import { Context } from 'hono';
import supabase from '../utils/supabase';
export const getInfluencers = async (c: Context) => {
// Get all influencers with filtering and pagination
const getInfluencers = async (c: Context) => {
try {
const {
platform,
@@ -20,12 +21,13 @@ export const getInfluencers = async (c: Context) => {
name,
platform,
profile_url,
external_id,
followers_count,
video_count,
platform_count,
created_at,
updated_at
`);
`, { count: 'exact' });
// Apply filters
if (platform) {
@@ -49,6 +51,7 @@ export const getInfluencers = async (c: Context) => {
const { data: influencers, error, count } = await query;
if (error) {
console.error('Error fetching influencers:', error);
return c.json({ error: error.message }, 500);
}
@@ -59,11 +62,13 @@ export const getInfluencers = async (c: Context) => {
offset: Number(offset)
});
} catch (error) {
console.error('Error in getInfluencers:', error);
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 {
const { influencer_id } = c.req.param();
@@ -74,32 +79,30 @@ export const getInfluencerById = async (c: Context) => {
name,
platform,
profile_url,
external_id,
followers_count,
video_count,
platform_count,
created_at,
updated_at,
posts (
post_id,
title,
description,
published_at
)
updated_at
`)
.eq('influencer_id', influencer_id)
.single();
if (error) {
console.error('Error fetching influencer:', error);
return c.json({ error: 'Influencer not found' }, 404);
}
return c.json(influencer);
} catch (error) {
console.error('Error in getInfluencerById:', error);
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 {
const { platform } = c.req.query();
@@ -114,6 +117,7 @@ export const getInfluencerStats = async (c: Context) => {
const { data: stats, error } = await query;
if (error) {
console.error('Error fetching influencer stats:', error);
return c.json({ error: error.message }, 500);
}
@@ -126,11 +130,212 @@ export const getInfluencerStats = async (c: Context) => {
),
average_videos: Math.round(
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);
} catch (error) {
console.error('Error in getInfluencerStats:', error);
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
};

View 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
};

View 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
};

View File

@@ -10,10 +10,11 @@ import postsRouter from './routes/posts';
import projectCommentsRouter from './routes/projectComments';
import commentsRouter from './routes/comments';
import influencersRouter from './routes/influencers';
import projectsRouter from './routes/projects';
import { connectRedis } from './utils/redis';
import { initClickHouse } from './utils/clickhouse';
import { initWorkers } from './utils/queue';
import { initDatabase, createSampleData, checkDatabaseConnection } from './utils/initDatabase';
import { checkDatabaseConnection } from './utils/initDatabase';
import { createSwaggerUI } from './swagger';
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
app.route('/api/auth', authRouter);
app.route('/api/analytics', analyticsRouter);
@@ -83,6 +48,7 @@ app.route('/api/posts', postsRouter);
app.route('/api/project-comments', projectCommentsRouter);
app.route('/api/comments', commentsRouter);
app.route('/api/influencers', influencersRouter);
app.route('/api/projects', projectsRouter);
// Swagger UI
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('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
console.log('🏗️ Initializing workers...');
const workers = {
@@ -138,8 +101,6 @@ const startServer = async () => {
console.log(`Server running at http://localhost:${port}`);
console.log(`Swagger UI available at http://localhost:${port}/swagger`);
console.log(`Initialize database at http://localhost:${port}/api/admin/init-db (POST)`);
console.log(`Create sample data at http://localhost:${port}/api/admin/create-sample-data (POST)`);
// Handle graceful shutdown
const shutdown = async () => {

View File

@@ -1,14 +1,16 @@
import { Hono } from 'hono';
import { getComments, createComment, deleteComment } from '../controllers/commentsController';
import { authMiddleware } from '../middlewares/auth';
import { getComments, getCommentById, createComment, updateComment, deleteComment } from '../controllers/commentsController';
const commentsRouter = new Hono();
// Public routes
commentsRouter.get('/', getComments);
commentsRouter.get('/:comment_id', getCommentById);
// Protected routes
commentsRouter.post('/', authMiddleware, createComment);
commentsRouter.put('/:comment_id', authMiddleware, updateComment);
commentsRouter.delete('/:comment_id', authMiddleware, deleteComment);
export default commentsRouter;

View File

@@ -1,11 +1,20 @@
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();
// Public routes
influencersRouter.get('/', getInfluencers);
influencersRouter.get('/stats', getInfluencerStats);
influencersRouter.get('/:influencer_id', getInfluencerById);
influencersRouter.get('/', influencersController.getInfluencers);
influencersRouter.get('/stats', influencersController.getInfluencerStats);
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;

View File

@@ -1,686 +1,17 @@
import { Hono } from 'hono';
import { authMiddleware } from '../middlewares/auth';
import supabase from '../utils/supabase';
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;
}
}
import postsController from '../controllers/postsController';
const postsRouter = new Hono();
// Apply auth middleware to most routes
postsRouter.use('*', authMiddleware);
// 创建新帖子
postsRouter.post('/', async (c) => {
try {
const {
influencer_id,
platform,
post_url,
title,
description,
published_at
} = await c.req.json();
if (!influencer_id || !platform || !post_url) {
return c.json({
error: 'influencer_id, platform, and post_url are required'
}, 400);
}
// 验证平台
const validPlatforms = ['youtube', 'instagram', 'tiktok', 'twitter', 'facebook'];
if (!validPlatforms.includes(platform)) {
return c.json({
error: `Invalid platform. Must be one of: ${validPlatforms.join(', ')}`
}, 400);
}
// 检查帖子URL是否已存在
const { data: existingPost, error: checkError } = await supabase
.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);
}
});
// Public routes
postsRouter.get('/', postsController.getPosts);
postsRouter.get('/:post_id', postsController.getPostById);
postsRouter.get('/:post_id/comments', postsController.getPostComments);
// Protected routes
postsRouter.post('/', authMiddleware, postsController.createPost);
postsRouter.put('/:post_id', authMiddleware, postsController.updatePost);
postsRouter.delete('/:post_id', authMiddleware, postsController.deletePost);
export default postsRouter;

View 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