This commit is contained in:
2025-03-07 17:45:17 +08:00
commit 936af0c4ec
114 changed files with 37662 additions and 0 deletions

395
backend/dist/routes/projectComments.js vendored Normal file
View 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;