init
This commit is contained in:
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