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