init
This commit is contained in:
453
backend/dist/routes/analytics.js
vendored
Normal file
453
backend/dist/routes/analytics.js
vendored
Normal file
@@ -0,0 +1,453 @@
|
||||
"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 clickhouse_1 = __importDefault(require("../utils/clickhouse"));
|
||||
const queue_1 = require("../utils/queue");
|
||||
const redis_1 = require("../utils/redis");
|
||||
const supabase_1 = __importDefault(require("../utils/supabase"));
|
||||
const analyticsRouter = new hono_1.Hono();
|
||||
// Apply auth middleware to all routes
|
||||
analyticsRouter.use('*', auth_1.authMiddleware);
|
||||
// Track a view event
|
||||
analyticsRouter.post('/view', async (c) => {
|
||||
try {
|
||||
const { content_id } = await c.req.json();
|
||||
const user = c.get('user');
|
||||
if (!content_id) {
|
||||
return c.json({ error: 'Content ID is required' }, 400);
|
||||
}
|
||||
// Get IP and user agent
|
||||
const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || '0.0.0.0';
|
||||
const userAgent = c.req.header('user-agent') || 'unknown';
|
||||
// Insert view event into ClickHouse
|
||||
await clickhouse_1.default.query({
|
||||
query: `
|
||||
INSERT INTO promote.view_events (user_id, content_id, ip, user_agent)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`,
|
||||
values: [
|
||||
user.id,
|
||||
content_id,
|
||||
ip,
|
||||
userAgent
|
||||
]
|
||||
});
|
||||
// Queue analytics processing job
|
||||
await (0, queue_1.addAnalyticsJob)('process_views', {
|
||||
user_id: user.id,
|
||||
content_id,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
// Increment view count in Redis cache
|
||||
const redis = await (0, redis_1.getRedisClient)();
|
||||
await redis.incr(`views:${content_id}`);
|
||||
return c.json({ message: 'View tracked successfully' });
|
||||
}
|
||||
catch (error) {
|
||||
console.error('View tracking error:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// Track a like event
|
||||
analyticsRouter.post('/like', async (c) => {
|
||||
try {
|
||||
const { content_id, action } = await c.req.json();
|
||||
const user = c.get('user');
|
||||
if (!content_id || !action) {
|
||||
return c.json({ error: 'Content ID and action are required' }, 400);
|
||||
}
|
||||
if (action !== 'like' && action !== 'unlike') {
|
||||
return c.json({ error: 'Action must be either "like" or "unlike"' }, 400);
|
||||
}
|
||||
// Insert like event into ClickHouse
|
||||
await clickhouse_1.default.query({
|
||||
query: `
|
||||
INSERT INTO promote.like_events (user_id, content_id, action)
|
||||
VALUES (?, ?, ?)
|
||||
`,
|
||||
values: [
|
||||
user.id,
|
||||
content_id,
|
||||
action === 'like' ? 1 : 2
|
||||
]
|
||||
});
|
||||
// Queue analytics processing job
|
||||
await (0, queue_1.addAnalyticsJob)('process_likes', {
|
||||
user_id: user.id,
|
||||
content_id,
|
||||
action,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
// Update like count in Redis cache
|
||||
const redis = await (0, redis_1.getRedisClient)();
|
||||
const likeKey = `likes:${content_id}`;
|
||||
if (action === 'like') {
|
||||
await redis.incr(likeKey);
|
||||
}
|
||||
else {
|
||||
await redis.decr(likeKey);
|
||||
}
|
||||
return c.json({ message: `${action} tracked successfully` });
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Like tracking error:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// Track a follow event
|
||||
analyticsRouter.post('/follow', async (c) => {
|
||||
try {
|
||||
const { followed_id, action } = await c.req.json();
|
||||
const user = c.get('user');
|
||||
if (!followed_id || !action) {
|
||||
return c.json({ error: 'Followed ID and action are required' }, 400);
|
||||
}
|
||||
if (action !== 'follow' && action !== 'unfollow') {
|
||||
return c.json({ error: 'Action must be either "follow" or "unfollow"' }, 400);
|
||||
}
|
||||
// Insert follower event into ClickHouse
|
||||
await clickhouse_1.default.query({
|
||||
query: `
|
||||
INSERT INTO promote.follower_events (follower_id, followed_id, action)
|
||||
VALUES (?, ?, ?)
|
||||
`,
|
||||
values: [
|
||||
user.id,
|
||||
followed_id,
|
||||
action === 'follow' ? 1 : 2
|
||||
]
|
||||
});
|
||||
// Queue analytics processing job
|
||||
await (0, queue_1.addAnalyticsJob)('process_followers', {
|
||||
follower_id: user.id,
|
||||
followed_id,
|
||||
action,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
// Update follower count in Redis cache
|
||||
const redis = await (0, redis_1.getRedisClient)();
|
||||
const followerKey = `followers:${followed_id}`;
|
||||
if (action === 'follow') {
|
||||
await redis.incr(followerKey);
|
||||
}
|
||||
else {
|
||||
await redis.decr(followerKey);
|
||||
}
|
||||
return c.json({ message: `${action} tracked successfully` });
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Follow tracking error:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// Get analytics for a content
|
||||
analyticsRouter.get('/content/:id', async (c) => {
|
||||
try {
|
||||
const contentId = c.req.param('id');
|
||||
// Get counts from Redis cache
|
||||
const redis = await (0, redis_1.getRedisClient)();
|
||||
const [views, likes] = await Promise.all([
|
||||
redis.get(`views:${contentId}`),
|
||||
redis.get(`likes:${contentId}`)
|
||||
]);
|
||||
return c.json({
|
||||
content_id: contentId,
|
||||
views: parseInt(views || '0'),
|
||||
likes: parseInt(likes || '0')
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Content analytics error:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// Get analytics for a user
|
||||
analyticsRouter.get('/user/:id', async (c) => {
|
||||
try {
|
||||
const userId = c.req.param('id');
|
||||
// Get follower count from Redis cache
|
||||
const redis = await (0, redis_1.getRedisClient)();
|
||||
const followers = await redis.get(`followers:${userId}`);
|
||||
// Get content view and like counts from ClickHouse
|
||||
const viewsResult = await clickhouse_1.default.query({
|
||||
query: `
|
||||
SELECT content_id, COUNT(*) as view_count
|
||||
FROM promote.view_events
|
||||
WHERE user_id = ?
|
||||
GROUP BY content_id
|
||||
`,
|
||||
values: [userId]
|
||||
});
|
||||
const likesResult = await clickhouse_1.default.query({
|
||||
query: `
|
||||
SELECT content_id, SUM(CASE WHEN action = 1 THEN 1 ELSE -1 END) as like_count
|
||||
FROM promote.like_events
|
||||
WHERE user_id = ?
|
||||
GROUP BY content_id
|
||||
`,
|
||||
values: [userId]
|
||||
});
|
||||
// Extract data from results
|
||||
const viewsData = 'rows' in viewsResult ? viewsResult.rows : [];
|
||||
const likesData = 'rows' in likesResult ? likesResult.rows : [];
|
||||
return c.json({
|
||||
user_id: userId,
|
||||
followers: parseInt(followers || '0'),
|
||||
content_analytics: {
|
||||
views: viewsData,
|
||||
likes: likesData
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('User analytics error:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 社群分析相关路由
|
||||
// 获取项目的顶级影响者
|
||||
analyticsRouter.get('/project/:id/top-influencers', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
// 从ClickHouse查询项目的顶级影响者
|
||||
const result = await clickhouse_1.default.query({
|
||||
query: `
|
||||
SELECT
|
||||
influencer_id,
|
||||
SUM(metric_value) AS total_views
|
||||
FROM events
|
||||
WHERE
|
||||
project_id = ? AND
|
||||
event_type = 'post_view_change'
|
||||
GROUP BY influencer_id
|
||||
ORDER BY total_views DESC
|
||||
LIMIT 10
|
||||
`,
|
||||
values: [projectId]
|
||||
});
|
||||
// 提取数据
|
||||
const influencerData = 'rows' in result ? result.rows : [];
|
||||
// 如果有数据,从Supabase获取影响者详细信息
|
||||
if (influencerData.length > 0) {
|
||||
const influencerIds = influencerData.map((item) => item.influencer_id);
|
||||
const { data: influencerDetails, error } = await supabase_1.default
|
||||
.from('influencers')
|
||||
.select('influencer_id, name, platform, followers_count, video_count')
|
||||
.in('influencer_id', influencerIds);
|
||||
if (error) {
|
||||
console.error('Error fetching influencer details:', error);
|
||||
return c.json({ error: 'Error fetching influencer details' }, 500);
|
||||
}
|
||||
// 合并数据
|
||||
const enrichedData = influencerData.map((item) => {
|
||||
const details = influencerDetails?.find((detail) => detail.influencer_id === item.influencer_id) || {};
|
||||
return {
|
||||
...item,
|
||||
...details
|
||||
};
|
||||
});
|
||||
return c.json(enrichedData);
|
||||
}
|
||||
return c.json(influencerData);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching top influencers:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取影响者的粉丝变化趋势(过去6个月)
|
||||
analyticsRouter.get('/influencer/:id/follower-trend', async (c) => {
|
||||
try {
|
||||
const influencerId = c.req.param('id');
|
||||
// 从ClickHouse查询影响者的粉丝变化趋势
|
||||
const result = await clickhouse_1.default.query({
|
||||
query: `
|
||||
SELECT
|
||||
toStartOfMonth(timestamp) AS month,
|
||||
SUM(metric_value) AS follower_change
|
||||
FROM events
|
||||
WHERE
|
||||
influencer_id = ? AND
|
||||
event_type = 'follower_change' AND
|
||||
timestamp >= subtractMonths(now(), 6)
|
||||
GROUP BY month
|
||||
ORDER BY month ASC
|
||||
`,
|
||||
values: [influencerId]
|
||||
});
|
||||
// 提取数据
|
||||
const trendData = 'rows' in result ? result.rows : [];
|
||||
return c.json({
|
||||
influencer_id: influencerId,
|
||||
follower_trend: trendData
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching follower trend:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取帖子的点赞变化(过去30天)
|
||||
analyticsRouter.get('/post/:id/like-trend', async (c) => {
|
||||
try {
|
||||
const postId = c.req.param('id');
|
||||
// 从ClickHouse查询帖子的点赞变化
|
||||
const result = await clickhouse_1.default.query({
|
||||
query: `
|
||||
SELECT
|
||||
toDate(timestamp) AS day,
|
||||
SUM(metric_value) AS like_change
|
||||
FROM events
|
||||
WHERE
|
||||
post_id = ? AND
|
||||
event_type = 'post_like_change' AND
|
||||
timestamp >= subtractDays(now(), 30)
|
||||
GROUP BY day
|
||||
ORDER BY day ASC
|
||||
`,
|
||||
values: [postId]
|
||||
});
|
||||
// 提取数据
|
||||
const trendData = 'rows' in result ? result.rows : [];
|
||||
return c.json({
|
||||
post_id: postId,
|
||||
like_trend: trendData
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching like trend:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取影响者详细信息
|
||||
analyticsRouter.get('/influencer/:id/details', async (c) => {
|
||||
try {
|
||||
const influencerId = c.req.param('id');
|
||||
// 从Supabase获取影响者详细信息
|
||||
const { data, error } = await supabase_1.default
|
||||
.from('influencers')
|
||||
.select('influencer_id, name, platform, profile_url, external_id, followers_count, video_count, platform_count, created_at')
|
||||
.eq('influencer_id', influencerId)
|
||||
.single();
|
||||
if (error) {
|
||||
console.error('Error fetching influencer details:', error);
|
||||
return c.json({ error: 'Error fetching influencer details' }, 500);
|
||||
}
|
||||
if (!data) {
|
||||
return c.json({ error: 'Influencer not found' }, 404);
|
||||
}
|
||||
return c.json(data);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching influencer details:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取影响者的帖子列表
|
||||
analyticsRouter.get('/influencer/:id/posts', async (c) => {
|
||||
try {
|
||||
const influencerId = c.req.param('id');
|
||||
// 从Supabase获取影响者的帖子列表
|
||||
const { data, error } = await supabase_1.default
|
||||
.from('posts')
|
||||
.select('post_id, influencer_id, platform, post_url, title, description, published_at, created_at')
|
||||
.eq('influencer_id', influencerId)
|
||||
.order('published_at', { ascending: false });
|
||||
if (error) {
|
||||
console.error('Error fetching influencer posts:', error);
|
||||
return c.json({ error: 'Error fetching influencer posts' }, 500);
|
||||
}
|
||||
return c.json(data || []);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching influencer posts:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取帖子的评论列表
|
||||
analyticsRouter.get('/post/:id/comments', async (c) => {
|
||||
try {
|
||||
const postId = c.req.param('id');
|
||||
// 从Supabase获取帖子的评论列表
|
||||
const { data, error } = await supabase_1.default
|
||||
.from('comments')
|
||||
.select('comment_id, post_id, user_id, content, sentiment_score, created_at')
|
||||
.eq('post_id', postId)
|
||||
.order('created_at', { ascending: false });
|
||||
if (error) {
|
||||
console.error('Error fetching post comments:', error);
|
||||
return c.json({ error: 'Error fetching post comments' }, 500);
|
||||
}
|
||||
return c.json(data || []);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching post comments:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取项目的平台分布
|
||||
analyticsRouter.get('/project/:id/platform-distribution', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
// 从ClickHouse查询项目的平台分布
|
||||
const result = await clickhouse_1.default.query({
|
||||
query: `
|
||||
SELECT
|
||||
platform,
|
||||
COUNT(DISTINCT influencer_id) AS influencer_count
|
||||
FROM events
|
||||
WHERE project_id = ?
|
||||
GROUP BY platform
|
||||
ORDER BY influencer_count DESC
|
||||
`,
|
||||
values: [projectId]
|
||||
});
|
||||
// 提取数据
|
||||
const distributionData = 'rows' in result ? result.rows : [];
|
||||
return c.json({
|
||||
project_id: projectId,
|
||||
platform_distribution: distributionData
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching platform distribution:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取项目的互动类型分布
|
||||
analyticsRouter.get('/project/:id/interaction-types', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
// 从ClickHouse查询项目的互动类型分布
|
||||
const result = await clickhouse_1.default.query({
|
||||
query: `
|
||||
SELECT
|
||||
event_type,
|
||||
COUNT(*) AS event_count,
|
||||
SUM(metric_value) AS total_value
|
||||
FROM events
|
||||
WHERE
|
||||
project_id = ? AND
|
||||
event_type IN ('click', 'comment', 'share')
|
||||
GROUP BY event_type
|
||||
ORDER BY event_count DESC
|
||||
`,
|
||||
values: [projectId]
|
||||
});
|
||||
// 提取数据
|
||||
const interactionData = 'rows' in result ? result.rows : [];
|
||||
return c.json({
|
||||
project_id: projectId,
|
||||
interaction_types: interactionData
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching interaction types:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
exports.default = analyticsRouter;
|
||||
140
backend/dist/routes/auth.js
vendored
Normal file
140
backend/dist/routes/auth.js
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
"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 authRouter = new hono_1.Hono();
|
||||
// Register a new user
|
||||
authRouter.post('/register', async (c) => {
|
||||
try {
|
||||
const { email, password, name } = await c.req.json();
|
||||
// Validate input
|
||||
if (!email || !password || !name) {
|
||||
return c.json({ error: 'Email, password, and name are required' }, 400);
|
||||
}
|
||||
// Register user with Supabase
|
||||
const { data: authData, error: authError } = await supabase_1.default.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
if (authError) {
|
||||
return c.json({ error: authError.message }, 400);
|
||||
}
|
||||
if (!authData.user) {
|
||||
return c.json({ error: 'Failed to create user' }, 500);
|
||||
}
|
||||
// Create user profile in the database
|
||||
const { error: profileError } = await supabase_1.default
|
||||
.from('users')
|
||||
.insert({
|
||||
id: authData.user.id,
|
||||
email: authData.user.email,
|
||||
name,
|
||||
created_at: new Date().toISOString(),
|
||||
});
|
||||
if (profileError) {
|
||||
// Attempt to clean up the auth user if profile creation fails
|
||||
await supabase_1.default.auth.admin.deleteUser(authData.user.id);
|
||||
return c.json({ error: profileError.message }, 500);
|
||||
}
|
||||
// Generate JWT token
|
||||
const token = (0, auth_1.generateToken)(authData.user.id, authData.user.email);
|
||||
return c.json({
|
||||
message: 'User registered successfully',
|
||||
user: {
|
||||
id: authData.user.id,
|
||||
email: authData.user.email,
|
||||
name,
|
||||
},
|
||||
token,
|
||||
}, 201);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// Login user
|
||||
authRouter.post('/login', async (c) => {
|
||||
try {
|
||||
const { email, password } = await c.req.json();
|
||||
const { data, error } = await supabase_1.default.auth.signInWithPassword({
|
||||
email,
|
||||
password
|
||||
});
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 400);
|
||||
}
|
||||
// 使用与 authMiddleware 一致的方式创建 JWT
|
||||
const token = (0, auth_1.generateToken)(data.user.id, data.user.email || '');
|
||||
// 只返回必要的用户信息和令牌
|
||||
return c.json({
|
||||
success: true,
|
||||
token,
|
||||
user: {
|
||||
id: data.user.id,
|
||||
email: data.user.email
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
return c.json({ error: 'Server error' }, 500);
|
||||
}
|
||||
});
|
||||
// Verify token
|
||||
authRouter.get('/verify', async (c) => {
|
||||
try {
|
||||
const token = c.req.header('Authorization')?.split(' ')[1];
|
||||
if (!token) {
|
||||
return c.json({ error: 'No token provided' }, 401);
|
||||
}
|
||||
const user = await (0, auth_1.verifySupabaseToken)(token);
|
||||
if (!user) {
|
||||
return c.json({ error: 'Invalid token' }, 401);
|
||||
}
|
||||
return c.json({
|
||||
message: 'Token is valid',
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// Refresh token
|
||||
authRouter.post('/refresh-token', async (c) => {
|
||||
try {
|
||||
const token = c.req.header('Authorization')?.split(' ')[1];
|
||||
if (!token) {
|
||||
return c.json({ error: 'No token provided' }, 401);
|
||||
}
|
||||
// 验证当前token
|
||||
const user = await (0, auth_1.verifySupabaseToken)(token);
|
||||
if (!user) {
|
||||
return c.json({ error: 'Invalid token' }, 401);
|
||||
}
|
||||
// 生成新token
|
||||
const newToken = (0, auth_1.generateToken)(user.id, user.email || '');
|
||||
return c.json({
|
||||
message: 'Token refreshed successfully',
|
||||
token: newToken,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Token refresh error:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
exports.default = authRouter;
|
||||
12
backend/dist/routes/comments.js
vendored
Normal file
12
backend/dist/routes/comments.js
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const hono_1 = require("hono");
|
||||
const commentsController_1 = require("../controllers/commentsController");
|
||||
const auth_1 = require("../middlewares/auth");
|
||||
const commentsRouter = new hono_1.Hono();
|
||||
// Public routes
|
||||
commentsRouter.get('/', commentsController_1.getComments);
|
||||
// Protected routes
|
||||
commentsRouter.post('/', auth_1.authMiddleware, commentsController_1.createComment);
|
||||
commentsRouter.delete('/:comment_id', auth_1.authMiddleware, commentsController_1.deleteComment);
|
||||
exports.default = commentsRouter;
|
||||
649
backend/dist/routes/community.js
vendored
Normal file
649
backend/dist/routes/community.js
vendored
Normal file
@@ -0,0 +1,649 @@
|
||||
"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 clickhouse_1 = __importDefault(require("../utils/clickhouse"));
|
||||
const supabase_1 = __importDefault(require("../utils/supabase"));
|
||||
const communityRouter = new hono_1.Hono();
|
||||
// Apply auth middleware to all routes
|
||||
communityRouter.use('*', auth_1.authMiddleware);
|
||||
// 创建新项目
|
||||
communityRouter.post('/projects', async (c) => {
|
||||
try {
|
||||
const { name, description, start_date, end_date } = await c.req.json();
|
||||
const user = c.get('user');
|
||||
if (!name) {
|
||||
return c.json({ error: 'Project name is required' }, 400);
|
||||
}
|
||||
// 在Supabase中创建项目
|
||||
const { data, error } = await supabase_1.default
|
||||
.from('projects')
|
||||
.insert({
|
||||
name,
|
||||
description,
|
||||
start_date,
|
||||
end_date,
|
||||
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({
|
||||
message: 'Project created successfully',
|
||||
project: data
|
||||
}, 201);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取项目列表
|
||||
communityRouter.get('/projects', async (c) => {
|
||||
try {
|
||||
const user = c.get('user');
|
||||
// 从Supabase获取项目列表
|
||||
const { data, error } = await supabase_1.default
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('created_by', user.id)
|
||||
.order('created_at', { ascending: false });
|
||||
if (error) {
|
||||
console.error('Error fetching projects:', error);
|
||||
return c.json({ error: 'Failed to fetch projects' }, 500);
|
||||
}
|
||||
return c.json(data || []);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching projects:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取项目详情
|
||||
communityRouter.get('/projects/:id', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
// 从Supabase获取项目详情
|
||||
const { data, error } = await supabase_1.default
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('id', projectId)
|
||||
.single();
|
||||
if (error) {
|
||||
console.error('Error fetching project:', error);
|
||||
return c.json({ error: 'Failed to fetch project' }, 500);
|
||||
}
|
||||
if (!data) {
|
||||
return c.json({ error: 'Project not found' }, 404);
|
||||
}
|
||||
return c.json(data);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching project:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 更新项目
|
||||
communityRouter.put('/projects/:id', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
const { name, description, start_date, end_date, status } = await c.req.json();
|
||||
const user = c.get('user');
|
||||
// 检查项目是否存在并属于当前用户
|
||||
const { data: existingProject, error: fetchError } = await supabase_1.default
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('id', projectId)
|
||||
.eq('created_by', user.id)
|
||||
.single();
|
||||
if (fetchError || !existingProject) {
|
||||
return c.json({ error: 'Project not found or you do not have permission to update it' }, 404);
|
||||
}
|
||||
// 更新项目
|
||||
const { data, error } = await supabase_1.default
|
||||
.from('projects')
|
||||
.update({
|
||||
name,
|
||||
description,
|
||||
start_date,
|
||||
end_date,
|
||||
status,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('id', projectId)
|
||||
.select()
|
||||
.single();
|
||||
if (error) {
|
||||
console.error('Error updating project:', error);
|
||||
return c.json({ error: 'Failed to update project' }, 500);
|
||||
}
|
||||
return c.json({
|
||||
message: 'Project updated successfully',
|
||||
project: data
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error updating project:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 删除项目
|
||||
communityRouter.delete('/projects/:id', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
const user = c.get('user');
|
||||
// 检查项目是否存在并属于当前用户
|
||||
const { data: existingProject, error: fetchError } = await supabase_1.default
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('id', projectId)
|
||||
.eq('created_by', user.id)
|
||||
.single();
|
||||
if (fetchError || !existingProject) {
|
||||
return c.json({ error: 'Project not found or you do not have permission to delete it' }, 404);
|
||||
}
|
||||
// 删除项目
|
||||
const { error } = await supabase_1.default
|
||||
.from('projects')
|
||||
.delete()
|
||||
.eq('id', projectId);
|
||||
if (error) {
|
||||
console.error('Error deleting project:', error);
|
||||
return c.json({ error: 'Failed to delete project' }, 500);
|
||||
}
|
||||
return c.json({
|
||||
message: 'Project deleted successfully'
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error deleting project:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 添加影响者到项目
|
||||
communityRouter.post('/projects/:id/influencers', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
const { influencer_id, platform, external_id, name, profile_url } = await c.req.json();
|
||||
const user = c.get('user');
|
||||
// 检查项目是否存在并属于当前用户
|
||||
const { data: existingProject, error: fetchError } = await supabase_1.default
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('id', projectId)
|
||||
.eq('created_by', user.id)
|
||||
.single();
|
||||
if (fetchError || !existingProject) {
|
||||
return c.json({ error: 'Project not found or you do not have permission to update it' }, 404);
|
||||
}
|
||||
// 检查影响者是否已存在
|
||||
let influencerData;
|
||||
if (influencer_id) {
|
||||
// 如果提供了影响者ID,检查是否存在
|
||||
const { data, error } = await supabase_1.default
|
||||
.from('influencers')
|
||||
.select('*')
|
||||
.eq('influencer_id', influencer_id)
|
||||
.single();
|
||||
if (!error && data) {
|
||||
influencerData = data;
|
||||
}
|
||||
}
|
||||
else if (external_id && platform) {
|
||||
// 如果提供了外部ID和平台,检查是否存在
|
||||
const { data, error } = await supabase_1.default
|
||||
.from('influencers')
|
||||
.select('*')
|
||||
.eq('external_id', external_id)
|
||||
.eq('platform', platform)
|
||||
.single();
|
||||
if (!error && data) {
|
||||
influencerData = data;
|
||||
}
|
||||
}
|
||||
// 如果影响者不存在,创建新的影响者
|
||||
if (!influencerData) {
|
||||
if (!name || !platform) {
|
||||
return c.json({ error: 'Name and platform are required for new influencers' }, 400);
|
||||
}
|
||||
const { data, error } = await supabase_1.default
|
||||
.from('influencers')
|
||||
.insert({
|
||||
name,
|
||||
platform,
|
||||
external_id,
|
||||
profile_url
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
if (error) {
|
||||
console.error('Error creating influencer:', error);
|
||||
return c.json({ error: 'Failed to create influencer' }, 500);
|
||||
}
|
||||
influencerData = data;
|
||||
}
|
||||
// 将影响者添加到项目
|
||||
const { data: projectInfluencer, error } = await supabase_1.default
|
||||
.from('project_influencers')
|
||||
.insert({
|
||||
project_id: projectId,
|
||||
influencer_id: influencerData.influencer_id
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
if (error) {
|
||||
console.error('Error adding influencer to project:', error);
|
||||
return c.json({ error: 'Failed to add influencer to project' }, 500);
|
||||
}
|
||||
return c.json({
|
||||
message: 'Influencer added to project successfully',
|
||||
project_influencer: projectInfluencer,
|
||||
influencer: influencerData
|
||||
}, 201);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error adding influencer to project:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取项目的影响者列表
|
||||
communityRouter.get('/projects/:id/influencers', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
// 从Supabase获取项目的影响者列表
|
||||
const { data, error } = await supabase_1.default
|
||||
.from('project_influencers')
|
||||
.select(`
|
||||
project_id,
|
||||
influencers (
|
||||
influencer_id,
|
||||
name,
|
||||
platform,
|
||||
profile_url,
|
||||
external_id,
|
||||
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);
|
||||
}
|
||||
// 格式化数据
|
||||
const influencers = data?.map(item => item.influencers) || [];
|
||||
return c.json(influencers);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching project influencers:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 从项目中移除影响者
|
||||
communityRouter.delete('/projects/:projectId/influencers/:influencerId', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('projectId');
|
||||
const influencerId = c.req.param('influencerId');
|
||||
const user = c.get('user');
|
||||
// 检查项目是否存在并属于当前用户
|
||||
const { data: existingProject, error: fetchError } = await supabase_1.default
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('id', projectId)
|
||||
.eq('created_by', user.id)
|
||||
.single();
|
||||
if (fetchError || !existingProject) {
|
||||
return c.json({ error: 'Project not found or you do not have permission to update it' }, 404);
|
||||
}
|
||||
// 从项目中移除影响者
|
||||
const { error } = await supabase_1.default
|
||||
.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.json({
|
||||
message: 'Influencer removed from project successfully'
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error removing influencer from project:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 添加事件数据
|
||||
communityRouter.post('/events', async (c) => {
|
||||
try {
|
||||
const { project_id, influencer_id, post_id, platform, event_type, metric_value, event_metadata } = await c.req.json();
|
||||
if (!project_id || !influencer_id || !platform || !event_type || metric_value === undefined) {
|
||||
return c.json({
|
||||
error: 'Project ID, influencer ID, platform, event type, and metric value are required'
|
||||
}, 400);
|
||||
}
|
||||
// 验证事件类型
|
||||
const validEventTypes = [
|
||||
'follower_change',
|
||||
'post_like_change',
|
||||
'post_view_change',
|
||||
'click',
|
||||
'comment',
|
||||
'share'
|
||||
];
|
||||
if (!validEventTypes.includes(event_type)) {
|
||||
return c.json({
|
||||
error: `Invalid event type. Must be one of: ${validEventTypes.join(', ')}`
|
||||
}, 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);
|
||||
}
|
||||
// 将事件数据插入ClickHouse
|
||||
await clickhouse_1.default.query({
|
||||
query: `
|
||||
INSERT INTO events (
|
||||
project_id,
|
||||
influencer_id,
|
||||
post_id,
|
||||
platform,
|
||||
event_type,
|
||||
metric_value,
|
||||
event_metadata
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
values: [
|
||||
project_id,
|
||||
influencer_id,
|
||||
post_id || null,
|
||||
platform,
|
||||
event_type,
|
||||
metric_value,
|
||||
event_metadata ? JSON.stringify(event_metadata) : '{}'
|
||||
]
|
||||
});
|
||||
return c.json({
|
||||
message: 'Event data added successfully'
|
||||
}, 201);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error adding event data:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 批量添加事件数据
|
||||
communityRouter.post('/events/batch', async (c) => {
|
||||
try {
|
||||
const { events } = await c.req.json();
|
||||
if (!Array.isArray(events) || events.length === 0) {
|
||||
return c.json({ error: 'Events array is required and must not be empty' }, 400);
|
||||
}
|
||||
// 验证事件类型和平台
|
||||
const validEventTypes = [
|
||||
'follower_change',
|
||||
'post_like_change',
|
||||
'post_view_change',
|
||||
'click',
|
||||
'comment',
|
||||
'share'
|
||||
];
|
||||
const validPlatforms = ['youtube', 'instagram', 'tiktok', 'twitter', 'facebook'];
|
||||
// 验证每个事件
|
||||
for (const event of events) {
|
||||
const { project_id, influencer_id, platform, event_type, metric_value } = event;
|
||||
if (!project_id || !influencer_id || !platform || !event_type || metric_value === undefined) {
|
||||
return c.json({
|
||||
error: 'Project ID, influencer ID, platform, event type, and metric value are required for all events'
|
||||
}, 400);
|
||||
}
|
||||
if (!validEventTypes.includes(event_type)) {
|
||||
return c.json({
|
||||
error: `Invalid event type: ${event_type}. Must be one of: ${validEventTypes.join(', ')}`
|
||||
}, 400);
|
||||
}
|
||||
if (!validPlatforms.includes(platform)) {
|
||||
return c.json({
|
||||
error: `Invalid platform: ${platform}. Must be one of: ${validPlatforms.join(', ')}`
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
// 准备批量插入数据
|
||||
const values = events.map(event => `(
|
||||
'${event.project_id}',
|
||||
'${event.influencer_id}',
|
||||
${event.post_id ? `'${event.post_id}'` : 'NULL'},
|
||||
'${event.platform}',
|
||||
'${event.event_type}',
|
||||
${event.metric_value},
|
||||
'${event.event_metadata ? JSON.stringify(event.event_metadata) : '{}'}'
|
||||
)`).join(',');
|
||||
// 批量插入事件数据
|
||||
await clickhouse_1.default.query({
|
||||
query: `
|
||||
INSERT INTO events (
|
||||
project_id,
|
||||
influencer_id,
|
||||
post_id,
|
||||
platform,
|
||||
event_type,
|
||||
metric_value,
|
||||
event_metadata
|
||||
) VALUES ${values}
|
||||
`
|
||||
});
|
||||
return c.json({
|
||||
message: `${events.length} events added successfully`
|
||||
}, 201);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error adding batch event data:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 添加帖子
|
||||
communityRouter.post('/posts', 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);
|
||||
}
|
||||
// 检查帖子是否已存在
|
||||
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, 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: data
|
||||
}, 201);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error creating post:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 添加评论
|
||||
communityRouter.post('/comments', async (c) => {
|
||||
try {
|
||||
const { post_id, user_id, content, sentiment_score } = await c.req.json();
|
||||
if (!post_id || !content) {
|
||||
return c.json({
|
||||
error: 'Post ID and content are required'
|
||||
}, 400);
|
||||
}
|
||||
// 创建新评论
|
||||
const { data, error } = await supabase_1.default
|
||||
.from('comments')
|
||||
.insert({
|
||||
post_id,
|
||||
user_id: user_id || c.get('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);
|
||||
}
|
||||
return c.json({
|
||||
message: 'Comment created successfully',
|
||||
comment: data
|
||||
}, 201);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error creating comment:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取项目的事件统计
|
||||
communityRouter.get('/projects/:id/event-stats', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
// 从ClickHouse查询项目的事件统计
|
||||
const result = await clickhouse_1.default.query({
|
||||
query: `
|
||||
SELECT
|
||||
event_type,
|
||||
COUNT(*) AS event_count,
|
||||
SUM(metric_value) AS total_value
|
||||
FROM events
|
||||
WHERE project_id = ?
|
||||
GROUP BY event_type
|
||||
ORDER BY event_count DESC
|
||||
`,
|
||||
values: [projectId]
|
||||
});
|
||||
// 提取数据
|
||||
const statsData = 'rows' in result ? result.rows : [];
|
||||
return c.json({
|
||||
project_id: projectId,
|
||||
event_stats: statsData
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching event stats:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取项目的时间趋势
|
||||
communityRouter.get('/projects/:id/time-trend', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
const { event_type, interval = 'day', days = '30' } = c.req.query();
|
||||
if (!event_type) {
|
||||
return c.json({ error: 'Event type is required' }, 400);
|
||||
}
|
||||
// 验证事件类型
|
||||
const validEventTypes = [
|
||||
'follower_change',
|
||||
'post_like_change',
|
||||
'post_view_change',
|
||||
'click',
|
||||
'comment',
|
||||
'share'
|
||||
];
|
||||
if (!validEventTypes.includes(event_type)) {
|
||||
return c.json({
|
||||
error: `Invalid event type. Must be one of: ${validEventTypes.join(', ')}`
|
||||
}, 400);
|
||||
}
|
||||
// 验证时间间隔
|
||||
const validIntervals = ['hour', 'day', 'week', 'month'];
|
||||
if (!validIntervals.includes(interval)) {
|
||||
return c.json({
|
||||
error: `Invalid interval. Must be one of: ${validIntervals.join(', ')}`
|
||||
}, 400);
|
||||
}
|
||||
// 构建时间间隔函数
|
||||
let timeFunction;
|
||||
switch (interval) {
|
||||
case 'hour':
|
||||
timeFunction = 'toStartOfHour';
|
||||
break;
|
||||
case 'day':
|
||||
timeFunction = 'toDate';
|
||||
break;
|
||||
case 'week':
|
||||
timeFunction = 'toStartOfWeek';
|
||||
break;
|
||||
case 'month':
|
||||
timeFunction = 'toStartOfMonth';
|
||||
break;
|
||||
}
|
||||
// 从ClickHouse查询项目的时间趋势
|
||||
const result = await clickhouse_1.default.query({
|
||||
query: `
|
||||
SELECT
|
||||
${timeFunction}(timestamp) AS time_period,
|
||||
SUM(metric_value) AS value
|
||||
FROM events
|
||||
WHERE
|
||||
project_id = ? AND
|
||||
event_type = ? AND
|
||||
timestamp >= subtractDays(now(), ?)
|
||||
GROUP BY time_period
|
||||
ORDER BY time_period ASC
|
||||
`,
|
||||
values: [projectId, event_type, parseInt(days)]
|
||||
});
|
||||
// 提取数据
|
||||
const trendData = 'rows' in result ? result.rows : [];
|
||||
return c.json({
|
||||
project_id: projectId,
|
||||
event_type,
|
||||
interval,
|
||||
days: parseInt(days),
|
||||
trend: trendData
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching time trend:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
exports.default = communityRouter;
|
||||
10
backend/dist/routes/influencers.js
vendored
Normal file
10
backend/dist/routes/influencers.js
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const hono_1 = require("hono");
|
||||
const influencersController_1 = require("../controllers/influencersController");
|
||||
const influencersRouter = new hono_1.Hono();
|
||||
// Public routes
|
||||
influencersRouter.get('/', influencersController_1.getInfluencers);
|
||||
influencersRouter.get('/stats', influencersController_1.getInfluencerStats);
|
||||
influencersRouter.get('/:influencer_id', influencersController_1.getInfluencerById);
|
||||
exports.default = influencersRouter;
|
||||
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;
|
||||
395
backend/dist/routes/projectComments.js
vendored
Normal file
395
backend/dist/routes/projectComments.js
vendored
Normal file
@@ -0,0 +1,395 @@
|
||||
"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 projectCommentsRouter = new hono_1.Hono();
|
||||
// Apply auth middleware to all routes
|
||||
projectCommentsRouter.use('*', auth_1.authMiddleware);
|
||||
// 获取项目的评论列表
|
||||
projectCommentsRouter.get('/projects/:id/comments', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
const { limit = '20', offset = '0', parent_id = null } = c.req.query();
|
||||
// 检查项目是否存在
|
||||
const { data: project, error: projectError } = await supabase_1.default
|
||||
.from('projects')
|
||||
.select('id, name')
|
||||
.eq('id', projectId)
|
||||
.single();
|
||||
if (projectError) {
|
||||
console.error('Error fetching project:', projectError);
|
||||
return c.json({ error: 'Project not found' }, 404);
|
||||
}
|
||||
// 构建评论查询
|
||||
let commentsQuery = supabase_1.default
|
||||
.from('project_comments')
|
||||
.select(`
|
||||
comment_id,
|
||||
project_id,
|
||||
user_id,
|
||||
content,
|
||||
sentiment_score,
|
||||
status,
|
||||
is_pinned,
|
||||
parent_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
user:user_id(id, email)
|
||||
`, { count: 'exact' });
|
||||
// 过滤条件
|
||||
commentsQuery = commentsQuery.eq('project_id', projectId);
|
||||
// 如果指定了父评论ID,则获取子评论
|
||||
if (parent_id) {
|
||||
commentsQuery = commentsQuery.eq('parent_id', parent_id);
|
||||
}
|
||||
else {
|
||||
// 否则获取顶级评论(没有父评论的评论)
|
||||
commentsQuery = commentsQuery.is('parent_id', null);
|
||||
}
|
||||
// 排序和分页
|
||||
const isPinned = parent_id ? false : true; // 只有顶级评论才考虑置顶
|
||||
if (isPinned) {
|
||||
commentsQuery = commentsQuery.order('is_pinned', { ascending: false });
|
||||
}
|
||||
commentsQuery = commentsQuery.order('created_at', { ascending: false });
|
||||
commentsQuery = commentsQuery.range(parseInt(offset), parseInt(offset) + parseInt(limit) - 1);
|
||||
// 执行查询
|
||||
const { data: comments, error: commentsError, count } = await commentsQuery;
|
||||
if (commentsError) {
|
||||
console.error('Error fetching project comments:', commentsError);
|
||||
return c.json({ error: 'Failed to fetch project comments' }, 500);
|
||||
}
|
||||
// 获取每个顶级评论的回复数量
|
||||
if (comments && !parent_id) {
|
||||
const commentIds = comments.map(comment => comment.comment_id);
|
||||
if (commentIds.length > 0) {
|
||||
// 手动构建SQL查询来计算每个父评论的回复数量
|
||||
const { data: replyCounts, error: replyCountError } = await supabase_1.default
|
||||
.rpc('get_reply_counts_for_comments', { parent_ids: commentIds });
|
||||
if (!replyCountError && replyCounts) {
|
||||
// 将回复数量添加到评论中
|
||||
for (const comment of comments) {
|
||||
const replyCountItem = replyCounts.find((r) => r.parent_id === comment.comment_id);
|
||||
comment.reply_count = replyCountItem ? replyCountItem.count : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.json({
|
||||
project,
|
||||
comments: comments || [],
|
||||
total: count || 0,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset)
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching project comments:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 添加评论到项目
|
||||
projectCommentsRouter.post('/projects/:id/comments', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
const user = c.get('user');
|
||||
const { content, sentiment_score = 0, parent_id = null } = await c.req.json();
|
||||
if (!content) {
|
||||
return c.json({ error: 'Comment content is required' }, 400);
|
||||
}
|
||||
// 检查项目是否存在
|
||||
const { data: project, error: projectError } = await supabase_1.default
|
||||
.from('projects')
|
||||
.select('id')
|
||||
.eq('id', projectId)
|
||||
.single();
|
||||
if (projectError) {
|
||||
console.error('Error fetching project:', projectError);
|
||||
return c.json({ error: 'Project not found' }, 404);
|
||||
}
|
||||
// 如果指定了父评论ID,检查父评论是否存在
|
||||
if (parent_id) {
|
||||
const { data: parentComment, error: parentError } = await supabase_1.default
|
||||
.from('project_comments')
|
||||
.select('comment_id')
|
||||
.eq('comment_id', parent_id)
|
||||
.eq('project_id', projectId)
|
||||
.single();
|
||||
if (parentError || !parentComment) {
|
||||
return c.json({ error: 'Parent comment not found' }, 404);
|
||||
}
|
||||
}
|
||||
// 创建评论
|
||||
const { data: comment, error: commentError } = await supabase_1.default
|
||||
.from('project_comments')
|
||||
.insert({
|
||||
project_id: projectId,
|
||||
user_id: user.id,
|
||||
content,
|
||||
sentiment_score,
|
||||
parent_id
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
if (commentError) {
|
||||
console.error('Error creating project comment:', commentError);
|
||||
return c.json({ error: 'Failed to create comment' }, 500);
|
||||
}
|
||||
// 记录评论事件到ClickHouse
|
||||
try {
|
||||
await clickhouse_1.default.query({
|
||||
query: `
|
||||
INSERT INTO events (
|
||||
project_id,
|
||||
event_type,
|
||||
metric_value,
|
||||
event_metadata
|
||||
) VALUES (?, 'project_comment', ?, ?)
|
||||
`,
|
||||
values: [
|
||||
projectId,
|
||||
1,
|
||||
JSON.stringify({
|
||||
comment_id: comment.comment_id,
|
||||
user_id: user.id,
|
||||
parent_id: parent_id || null,
|
||||
content: content.substring(0, 100), // 只存储部分内容以减小数据量
|
||||
sentiment_score: sentiment_score
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
catch (chError) {
|
||||
console.error('Error recording project comment event:', chError);
|
||||
// 继续执行,不中断主流程
|
||||
}
|
||||
return c.json({
|
||||
message: 'Comment added successfully',
|
||||
comment
|
||||
}, 201);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error adding project comment:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 更新项目评论
|
||||
projectCommentsRouter.put('/comments/:id', async (c) => {
|
||||
try {
|
||||
const commentId = c.req.param('id');
|
||||
const user = c.get('user');
|
||||
const { content, sentiment_score, is_pinned } = await c.req.json();
|
||||
// 检查评论是否存在且属于当前用户或用户是项目拥有者
|
||||
const { data: comment, error: fetchError } = await supabase_1.default
|
||||
.from('project_comments')
|
||||
.select(`
|
||||
comment_id,
|
||||
project_id,
|
||||
user_id,
|
||||
projects!inner(created_by)
|
||||
`)
|
||||
.eq('comment_id', commentId)
|
||||
.single();
|
||||
if (fetchError || !comment) {
|
||||
return c.json({ error: 'Comment not found' }, 404);
|
||||
}
|
||||
// 确保我们能够安全地访问projects中的created_by字段
|
||||
const projectOwner = comment.projects &&
|
||||
Array.isArray(comment.projects) &&
|
||||
comment.projects.length > 0 ?
|
||||
comment.projects[0].created_by : null;
|
||||
// 检查用户是否有权限更新评论
|
||||
const isCommentOwner = comment.user_id === user.id;
|
||||
const isProjectOwner = projectOwner === user.id;
|
||||
if (!isCommentOwner && !isProjectOwner) {
|
||||
return c.json({
|
||||
error: 'You do not have permission to update this comment'
|
||||
}, 403);
|
||||
}
|
||||
// 准备更新数据
|
||||
const updateData = {};
|
||||
// 评论创建者可以更新内容和情感分数
|
||||
if (isCommentOwner) {
|
||||
if (content !== undefined) {
|
||||
updateData.content = content;
|
||||
}
|
||||
if (sentiment_score !== undefined) {
|
||||
updateData.sentiment_score = sentiment_score;
|
||||
}
|
||||
}
|
||||
// 项目所有者可以更新状态和置顶
|
||||
if (isProjectOwner) {
|
||||
if (is_pinned !== undefined) {
|
||||
updateData.is_pinned = is_pinned;
|
||||
}
|
||||
}
|
||||
// 更新时间
|
||||
updateData.updated_at = new Date().toISOString();
|
||||
// 如果没有内容要更新,返回错误
|
||||
if (Object.keys(updateData).length === 1) { // 只有updated_at
|
||||
return c.json({ error: 'No valid fields to update' }, 400);
|
||||
}
|
||||
// 更新评论
|
||||
const { data: updatedComment, error } = await supabase_1.default
|
||||
.from('project_comments')
|
||||
.update(updateData)
|
||||
.eq('comment_id', commentId)
|
||||
.select()
|
||||
.single();
|
||||
if (error) {
|
||||
console.error('Error updating project 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 project comment:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 删除项目评论
|
||||
projectCommentsRouter.delete('/comments/:id', async (c) => {
|
||||
try {
|
||||
const commentId = c.req.param('id');
|
||||
const user = c.get('user');
|
||||
// 检查评论是否存在且属于当前用户或用户是项目拥有者
|
||||
const { data: comment, error: fetchError } = await supabase_1.default
|
||||
.from('project_comments')
|
||||
.select(`
|
||||
comment_id,
|
||||
project_id,
|
||||
user_id,
|
||||
projects!inner(created_by)
|
||||
`)
|
||||
.eq('comment_id', commentId)
|
||||
.single();
|
||||
if (fetchError || !comment) {
|
||||
return c.json({ error: 'Comment not found' }, 404);
|
||||
}
|
||||
// 确保我们能够安全地访问projects中的created_by字段
|
||||
const projectOwner = comment.projects &&
|
||||
Array.isArray(comment.projects) &&
|
||||
comment.projects.length > 0 ?
|
||||
comment.projects[0].created_by : null;
|
||||
// 检查用户是否有权限删除评论
|
||||
const isCommentOwner = comment.user_id === user.id;
|
||||
const isProjectOwner = projectOwner === user.id;
|
||||
if (!isCommentOwner && !isProjectOwner) {
|
||||
return c.json({
|
||||
error: 'You do not have permission to delete this comment'
|
||||
}, 403);
|
||||
}
|
||||
// 删除评论
|
||||
const { error } = await supabase_1.default
|
||||
.from('project_comments')
|
||||
.delete()
|
||||
.eq('comment_id', commentId);
|
||||
if (error) {
|
||||
console.error('Error deleting project comment:', error);
|
||||
return c.json({ error: 'Failed to delete comment' }, 500);
|
||||
}
|
||||
return c.json({
|
||||
message: 'Comment deleted successfully'
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error deleting project comment:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
// 获取项目评论的统计信息
|
||||
projectCommentsRouter.get('/projects/:id/comments/stats', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
// 检查项目是否存在
|
||||
const { data: project, error: projectError } = await supabase_1.default
|
||||
.from('projects')
|
||||
.select('id, name')
|
||||
.eq('id', projectId)
|
||||
.single();
|
||||
if (projectError) {
|
||||
console.error('Error fetching project:', projectError);
|
||||
return c.json({ error: 'Project not found' }, 404);
|
||||
}
|
||||
// 从Supabase获取评论总数
|
||||
const { count } = await supabase_1.default
|
||||
.from('project_comments')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('project_id', projectId);
|
||||
// 从Supabase获取情感分析统计
|
||||
const { data: sentimentStats } = await supabase_1.default
|
||||
.from('project_comments')
|
||||
.select('sentiment_score')
|
||||
.eq('project_id', projectId);
|
||||
let averageSentiment = 0;
|
||||
let positiveCount = 0;
|
||||
let neutralCount = 0;
|
||||
let negativeCount = 0;
|
||||
if (sentimentStats && sentimentStats.length > 0) {
|
||||
// 计算平均情感分数
|
||||
const totalSentiment = sentimentStats.reduce((acc, curr) => acc + (curr.sentiment_score || 0), 0);
|
||||
averageSentiment = totalSentiment / sentimentStats.length;
|
||||
// 分类情感分数
|
||||
sentimentStats.forEach(stat => {
|
||||
const score = stat.sentiment_score || 0;
|
||||
if (score > 0.3) {
|
||||
positiveCount++;
|
||||
}
|
||||
else if (score < -0.3) {
|
||||
negativeCount++;
|
||||
}
|
||||
else {
|
||||
neutralCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
let timeTrend = [];
|
||||
try {
|
||||
const result = await clickhouse_1.default.query({
|
||||
query: `
|
||||
SELECT
|
||||
toDate(timestamp) as date,
|
||||
count() as comment_count
|
||||
FROM events
|
||||
WHERE
|
||||
project_id = ? AND
|
||||
event_type = 'project_comment' AND
|
||||
timestamp >= subtractDays(now(), 30)
|
||||
GROUP BY date
|
||||
ORDER BY date ASC
|
||||
`,
|
||||
values: [projectId]
|
||||
});
|
||||
timeTrend = 'rows' in result ? result.rows : [];
|
||||
}
|
||||
catch (chError) {
|
||||
console.error('Error fetching comment time trend:', chError);
|
||||
// 继续执行,返回空趋势数据
|
||||
}
|
||||
return c.json({
|
||||
project_id: projectId,
|
||||
project_name: project.name,
|
||||
total_comments: count || 0,
|
||||
sentiment: {
|
||||
average: averageSentiment,
|
||||
positive: positiveCount,
|
||||
neutral: neutralCount,
|
||||
negative: negativeCount
|
||||
},
|
||||
time_trend: timeTrend
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching project comment stats:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
exports.default = projectCommentsRouter;
|
||||
Reference in New Issue
Block a user