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