396 lines
15 KiB
JavaScript
396 lines
15 KiB
JavaScript
"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;
|