Files
promote/backend/dist/routes/posts.js
2025-03-07 18:04:27 +08:00

585 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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;