diff --git a/Requirements.md b/Requirements.md index 69c3033..be5d223 100644 --- a/Requirements.md +++ b/Requirements.md @@ -56,7 +56,7 @@ - 条形长度直观反映各平台占比 - 帮助团队了解哪些平台效果更好 -# 审核状态分布 [先不做] +# 审核状态分布 [已实现] - 环形图展示内容审核状态的分布情况 - 包括三种状态:已核准、待审核、已拒绝 diff --git a/backend/src/controllers/analyticsController.ts b/backend/src/controllers/analyticsController.ts index 22fac4c..5ea2993 100644 --- a/backend/src/controllers/analyticsController.ts +++ b/backend/src/controllers/analyticsController.ts @@ -678,6 +678,82 @@ export class AnalyticsController { }, 500); } } + + /** + * 获取内容审核状态分布数据 + * 返回已批准、待审核和已拒绝内容的数量和比例 + * + * @param c Hono Context + * @returns Response with moderation status distribution data + */ + async getModerationStatus(c: Context) { + const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`; + const startTime = Date.now(); + + try { + // 获取查询参数 + const timeRange = c.req.query('timeRange') || '30'; // 默认30天 + const projectId = c.req.query('projectId'); // 可选项目过滤 + const contentType = c.req.query('contentType') || 'all'; // 内容类型:post, comment, all + + logger.info(`[${requestId}] Moderation status distribution request received`, { + timeRange, + projectId, + contentType, + userAgent: c.req.header('user-agent'), + ip: c.req.header('x-forwarded-for') || 'unknown' + }); + + // 验证时间范围 + if (!['7', '30', '90'].includes(timeRange)) { + logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`); + return c.json({ + success: false, + error: 'Invalid timeRange. Must be 7, 30, or 90.' + }, 400); + } + + // 验证内容类型 + if (!['post', 'comment', 'all'].includes(contentType)) { + logger.warn(`[${requestId}] Invalid contentType: ${contentType}`); + return c.json({ + success: false, + error: 'Invalid contentType. Must be post, comment, or all.' + }, 400); + } + + // 获取审核状态分布数据 + const data = await analyticsService.getModerationStatusDistribution( + parseInt(timeRange, 10), + projectId, + contentType + ); + + // 返回成功响应 + const duration = Date.now() - startTime; + logger.info(`[${requestId}] Moderation status distribution response sent successfully`, { + duration, + statuses: Object.keys(data.statuses), + total: data.total + }); + + return c.json({ + success: true, + data: data + }); + } catch (error) { + // 记录错误 + const duration = Date.now() - startTime; + logger.error(`[${requestId}] Error fetching moderation status distribution (${duration}ms)`, error); + + // 返回错误响应 + return c.json({ + success: false, + error: 'Failed to fetch moderation status distribution data', + message: error instanceof Error ? error.message : 'Unknown error' + }, 500); + } + } } // Export singleton instance diff --git a/backend/src/routes/analytics.ts b/backend/src/routes/analytics.ts index 261d0e9..6f11402 100644 --- a/backend/src/routes/analytics.ts +++ b/backend/src/routes/analytics.ts @@ -47,4 +47,7 @@ analyticsRouter.get('/sentiment-analysis', (c) => analyticsController.getSentime // Add popular posts route analyticsRouter.get('/popular-posts', (c) => analyticsController.getPopularPosts(c)); +// Add moderation status distribution route +analyticsRouter.get('/moderation-status', (c) => analyticsController.getModerationStatus(c)); + export default analyticsRouter; \ No newline at end of file diff --git a/backend/src/services/analyticsService.ts b/backend/src/services/analyticsService.ts index eb20e5b..f2797c6 100644 --- a/backend/src/services/analyticsService.ts +++ b/backend/src/services/analyticsService.ts @@ -179,6 +179,34 @@ export interface PopularPostsResponse { total: number; } +/** + * Sentiment analysis response + */ +export interface SentimentAnalysisResponse { + positive_percentage: number; + neutral_percentage: number; + negative_percentage: number; + average_score: number; + total: number; +} + +/** + * Moderation status response + */ +export interface ModerationStatusResponse { + statuses: { + approved: number; + pending: number; + rejected: number; + }; + percentages: { + approved: number; + pending: number; + rejected: number; + }; + total: number; +} + /** * Analytics service for KOL performance data */ @@ -1652,6 +1680,127 @@ export class AnalyticsService { }; } } + + /** + * 获取内容审核状态分布数据 + * @param timeRange 时间范围(天数) + * @param projectId 可选项目ID过滤 + * @param contentType 内容类型过滤(post, comment, all) + * @returns 审核状态分布数据 + */ + async getModerationStatusDistribution( + timeRange: number, + projectId?: string, + contentType: string = 'all' + ): Promise { + const startTime = Date.now(); + logger.info('Fetching moderation status distribution', { timeRange, projectId, contentType }); + + try { + // 计算时间范围 + const currentDate = new Date(); + const pastDate = new Date(); + pastDate.setDate(currentDate.getDate() - timeRange); + + // 格式化日期 + const currentDateStr = this.formatDateForClickhouse(currentDate); + const pastDateStr = this.formatDateForClickhouse(pastDate); + + // 构建过滤条件 + const filters: string[] = []; + filters.push(`date BETWEEN '${pastDateStr}' AND '${currentDateStr}'`); + + if (projectId) { + filters.push(`project_id = '${projectId}'`); + } + + // 基于内容类型的过滤 + let typeFilter = ''; + if (contentType === 'post') { + typeFilter = "AND content_type IN ('video', 'image', 'text', 'story', 'reel', 'live')"; + } else if (contentType === 'comment') { + typeFilter = "AND event_type = 'comment'"; + } + + const filterCondition = filters.join(' AND '); + + // 查询审核状态分布 + const query = ` + SELECT + content_status, + count() as status_count + FROM + events + WHERE + ${filterCondition} + AND content_status IN ('approved', 'pending', 'rejected') + ${typeFilter} + GROUP BY + content_status + `; + + logger.debug('Executing ClickHouse query for moderation status distribution', { + query: query.replace(/\n\s+/g, ' ').trim() + }); + + // 执行查询 + const results = await this.executeClickhouseQuery(query); + + // 初始化结果对象 + const statusCounts = { + approved: 0, + pending: 0, + rejected: 0 + }; + + // 解析结果 + results.forEach(row => { + const status = row.content_status.toLowerCase(); + if (status in statusCounts) { + statusCounts[status as keyof typeof statusCounts] = Number(row.status_count); + } + }); + + // 计算总数和百分比 + const total = statusCounts.approved + statusCounts.pending + statusCounts.rejected; + + const calculatePercentage = (count: number): number => { + if (total === 0) return 0; + return parseFloat(((count / total) * 100).toFixed(1)); + }; + + const statusPercentages = { + approved: calculatePercentage(statusCounts.approved), + pending: calculatePercentage(statusCounts.pending), + rejected: calculatePercentage(statusCounts.rejected) + }; + + const result: ModerationStatusResponse = { + statuses: statusCounts, + percentages: statusPercentages, + total + }; + + const duration = Date.now() - startTime; + logger.info('Moderation status distribution fetched successfully', { + duration, + total, + statusDistribution: statusCounts + }); + + return result; + } catch (error) { + const duration = Date.now() - startTime; + logger.error(`Error in getModerationStatusDistribution (${duration}ms)`, error); + + // 发生错误时返回默认响应 + return { + statuses: { approved: 0, pending: 0, rejected: 0 }, + percentages: { approved: 0, pending: 0, rejected: 0 }, + total: 0 + }; + } + } } // Export singleton instance diff --git a/backend/src/swagger/index.ts b/backend/src/swagger/index.ts index 12afcd7..d3a5337 100644 --- a/backend/src/swagger/index.ts +++ b/backend/src/swagger/index.ts @@ -2923,14 +2923,14 @@ export const openAPISpec = { }, '/api/analytics/popular-posts': { get: { - summary: '获取热门文章数据', - description: '返回按互动数量或互动率排序的热门帖文列表', + summary: '获取热门帖文数据', + description: '返回按互动数量或互动率排序的热门帖文', tags: ['Analytics'], parameters: [ { name: 'timeRange', in: 'query', - description: '时间范围(天)', + description: '时间范围(天)', schema: { type: 'string', enum: ['7', '30', '90'], @@ -2940,7 +2940,7 @@ export const openAPISpec = { { name: 'projectId', in: 'query', - description: '项目ID过滤', + description: '项目ID', schema: { type: 'string' } @@ -2948,10 +2948,9 @@ export const openAPISpec = { { name: 'platform', in: 'query', - description: '平台过滤', + description: '平台', schema: { - type: 'string', - enum: ['Twitter', 'Instagram', 'TikTok', 'Facebook', 'YouTube'] + type: 'string' } }, { @@ -2967,7 +2966,7 @@ export const openAPISpec = { { name: 'limit', in: 'query', - description: '返回数量', + description: '返回数量限制', schema: { type: 'integer', default: 10, @@ -2977,36 +2976,58 @@ export const openAPISpec = { ], responses: { '200': { - description: '成功响应', + description: '热门帖文数据', content: { 'application/json': { schema: { type: 'object', properties: { - success: { type: 'boolean', example: true }, + success: { + type: 'boolean' + }, data: { type: 'array', items: { type: 'object', properties: { - post_id: { type: 'string', example: 'post_123' }, - title: { type: 'string', example: '新产品发布' }, - platform: { type: 'string', example: 'Instagram' }, - influencer_id: { type: 'string', example: 'inf_456' }, - influencer_name: { type: 'string', example: '时尚KOL' }, - publish_date: { type: 'string', example: '2025-03-10 10:30:00' }, - engagement_count: { type: 'number', example: 2350 }, - views_count: { type: 'number', example: 15600 }, - engagement_rate: { type: 'number', example: 0.1506 }, - is_high_engagement: { type: 'boolean', example: true } + title: { + type: 'string' + }, + platform: { + type: 'string' + }, + influencer_name: { + type: 'string' + }, + publish_date: { + type: 'string', + format: 'date-time' + }, + engagement_count: { + type: 'integer' + }, + views_count: { + type: 'integer' + }, + engagement_rate: { + type: 'number', + format: 'float' + }, + is_high_engagement: { + type: 'boolean' + } } } }, metadata: { type: 'object', properties: { - total: { type: 'number', example: 45 }, - high_engagement_count: { type: 'number', example: 8 } + total: { + type: 'integer' + }, + high_engagement_count: { + type: 'integer' + } } } } @@ -3015,33 +3036,144 @@ export const openAPISpec = { } }, '400': { - description: '参数错误', + description: '请求参数错误', content: { 'application/json': { schema: { - type: 'object', - properties: { - success: { type: 'boolean', example: false }, - error: { type: 'string', example: 'Invalid sortBy. Must be engagement_count or engagement_rate.' } - } + $ref: '#/components/schemas/Error' } } } }, '500': { description: '服务器错误', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + } + } + } + }, + '/api/analytics/moderation-status': { + get: { + summary: '获取内容审核状态分布', + description: '返回已批准、待审核和已拒绝内容的数量和比例', + tags: ['Analytics'], + parameters: [ + { + name: 'timeRange', + in: 'query', + description: '时间范围(天)', + schema: { + type: 'string', + enum: ['7', '30', '90'], + default: '30' + } + }, + { + name: 'projectId', + in: 'query', + description: '项目ID', + schema: { + type: 'string' + } + }, + { + name: 'contentType', + in: 'query', + description: '内容类型', + schema: { + type: 'string', + enum: ['post', 'comment', 'all'], + default: 'all' + } + } + ], + responses: { + '200': { + description: '审核状态分布数据', content: { 'application/json': { schema: { type: 'object', properties: { - success: { type: 'boolean', example: false }, - error: { type: 'string', example: 'Failed to fetch popular posts data' }, - message: { type: 'string', example: 'Internal server error' } + success: { + type: 'boolean' + }, + data: { + type: 'object', + properties: { + statuses: { + type: 'object', + properties: { + approved: { + type: 'integer', + description: '已批准内容数量' + }, + pending: { + type: 'integer', + description: '待审核内容数量' + }, + rejected: { + type: 'integer', + description: '已拒绝内容数量' + } + } + }, + percentages: { + type: 'object', + properties: { + approved: { + type: 'number', + format: 'float', + description: '已批准内容百分比' + }, + pending: { + type: 'number', + format: 'float', + description: '待审核内容百分比' + }, + rejected: { + type: 'number', + format: 'float', + description: '已拒绝内容百分比' + } + } + }, + total: { + type: 'integer', + description: '内容总数' + } + } + } } } } } + }, + '400': { + description: '请求参数错误', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + }, + '500': { + description: '服务器错误', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } } } } diff --git a/web/src/components/Analytics.tsx b/web/src/components/Analytics.tsx index eb2bcf2..163225d 100644 --- a/web/src/components/Analytics.tsx +++ b/web/src/components/Analytics.tsx @@ -193,6 +193,25 @@ interface CommentTrendResponse { error?: string; } +// 添加审核状态分布API响应接口 +interface ModerationStatusResponse { + success: boolean; + data: { + statuses: { + approved: number; + pending: number; + rejected: number; + }; + percentages: { + approved: number; + pending: number; + rejected: number; + }; + total: number; + }; + error?: string; +} + // 添加热门文章API响应接口 interface PopularPostsResponse { success: boolean; @@ -238,6 +257,10 @@ const Analytics: React.FC = () => { const [trendError, setTrendError] = useState(null); const [maxTimelineCount, setMaxTimelineCount] = useState(1); // 设置默认值为1避免除以零 + // 添加审核状态分布相关状态 + const [moderationLoading, setModerationLoading] = useState(true); + const [moderationError, setModerationError] = useState(null); + // 添加项目相关状态 const [projects, setProjects] = useState([ { id: '1', name: '项目 1', description: '示例项目 1' }, @@ -290,6 +313,102 @@ const Analytics: React.FC = () => { const [postsLoading, setPostsLoading] = useState(true); const [postsError, setPostsError] = useState(null); + // 添加获取审核状态分布数据的函数 + const fetchModerationStatus = async () => { + try { + setModerationLoading(true); + setModerationError(null); + + // 构建API URL + const url = `http://localhost:4000/api/analytics/moderation-status?timeRange=${timeRange}`; + + // 添加项目过滤参数(如果选择了特定项目) + const urlWithFilters = selectedProject !== 'all' + ? `${url}&projectId=${selectedProject}` + : url; + + // 添加内容类型 + const finalUrl = `${urlWithFilters}&contentType=all`; + + console.log('请求审核状态分布数据URL:', finalUrl); + + // 添加认证头 + const authToken = 'eyJhbGciOiJIUzI1NiIsImtpZCI6Inl3blNGYnRBOGtBUnl4UmUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3h0cWhsdXpvcm5hemxta29udWNyLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiI1YjQzMThiZi0yMWE4LTQ3YWMtOGJmYS0yYThmOGVmOWMwZmIiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzQxNjI3ODkyLCJpYXQiOjE3NDE2MjQyOTIsImVtYWlsIjoidml0YWxpdHltYWlsZ0BnbWFpbC5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsX3ZlcmlmaWVkIjp0cnVlfSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc0MTYyNDI5Mn1dLCJzZXNzaW9uX2lkIjoiODlmYjg0YzktZmEzYy00YmVlLTk0MDQtNjI1MjE0OGIyMzVlIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.VuUX2yhqN-FZseKL8fQG91i1cohfRqW2m1Z8CIWhZuk'; + + const response = await fetch(finalUrl, { + headers: { + 'accept': 'application/json', + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const result = await response.json() as ModerationStatusResponse; + console.log('成功获取审核状态分布数据:', result); + + if (result.success) { + // 将API返回的数据映射到AnalyticsData结构 + const mappedData: AnalyticsData[] = [ + { + name: 'approved', + value: result.data.statuses.approved, + color: '#10B981', + percentage: result.data.percentages.approved + }, + { + name: 'pending', + value: result.data.statuses.pending, + color: '#F59E0B', + percentage: result.data.percentages.pending + }, + { + name: 'rejected', + value: result.data.statuses.rejected, + color: '#EF4444', + percentage: result.data.percentages.rejected + }, + ]; + + // 更新状态 + setStatusData(mappedData); + } else { + setModerationError(result.error || '获取审核状态分布数据失败'); + console.error('API调用失败:', result.error || '未知错误'); + + // 设置默认数据 + setStatusData([ + { name: 'approved', value: 45, color: '#10B981', percentage: 45 }, + { name: 'pending', value: 30, color: '#F59E0B', percentage: 30 }, + { name: 'rejected', value: 25, color: '#EF4444', percentage: 25 } + ]); + } + } else { + const errorText = await response.text(); + setModerationError(`获取失败 (${response.status}): ${errorText}`); + console.error('获取审核状态分布数据失败,HTTP状态:', response.status, errorText); + + // 设置默认数据 + setStatusData([ + { name: 'approved', value: 45, color: '#10B981', percentage: 45 }, + { name: 'pending', value: 30, color: '#F59E0B', percentage: 30 }, + { name: 'rejected', value: 25, color: '#EF4444', percentage: 25 } + ]); + } + } catch (error) { + setModerationError(`获取审核状态分布数据时发生错误: ${error instanceof Error ? error.message : String(error)}`); + console.error('获取审核状态分布数据时发生错误:', error); + + // 设置默认数据 + setStatusData([ + { name: 'approved', value: 45, color: '#10B981', percentage: 45 }, + { name: 'pending', value: 30, color: '#F59E0B', percentage: 30 }, + { name: 'rejected', value: 25, color: '#EF4444', percentage: 25 } + ]); + } finally { + setModerationLoading(false); + } + }; + // 获取KOL概览数据 const fetchKolOverviewData = async () => { setKolLoading(true); @@ -380,17 +499,15 @@ const Analytics: React.FC = () => { // 获取热门文章数据 fetchPopularPosts(); + // 获取审核状态分布数据 + fetchModerationStatus(); + const fetchAnalyticsData = async () => { try { setLoading(true); - - // Set mock status data - setStatusData([ - { name: 'approved', value: 45, color: '#10B981' }, - { name: 'pending', value: 30, color: '#F59E0B' }, - { name: 'rejected', value: 25, color: '#EF4444' } - ]); + // 不再使用模拟审核状态数据,通过API获取 + // 已移至 fetchModerationStatus 函数中 // 从API获取漏斗数据 try { @@ -436,15 +553,15 @@ const Analytics: React.FC = () => { // 如果API返回了模拟数据标志 if (result.is_mock_data) { console.info('注意: 使用的是模拟漏斗数据'); - } - } else { - console.error('API返回的数据格式不正确:', result); - // 使用模拟数据作为后备 - setFallbackFunnelData(); } } else { - console.error('API调用失败:', result.error || '未知错误'); + console.error('API返回的数据格式不正确:', result); // 使用模拟数据作为后备 + setFallbackFunnelData(); + } + } else { + console.error('API调用失败:', result.error || '未知错误'); + // 使用模拟数据作为后备 setFallbackFunnelData(); } } else { @@ -469,14 +586,14 @@ const Analytics: React.FC = () => { // 添加辅助函数,设置后备漏斗数据 const setFallbackFunnelData = () => { - setFunnelData([ - { stage: 'Awareness', count: 10000, rate: 100 }, - { stage: 'Interest', count: 7500, rate: 75 }, - { stage: 'Consideration', count: 5000, rate: 50 }, - { stage: 'Intent', count: 3000, rate: 30 }, - { stage: 'Evaluation', count: 2000, rate: 20 }, - { stage: 'Purchase', count: 1000, rate: 10 } - ]); + setFunnelData([ + { stage: 'Awareness', count: 10000, rate: 100 }, + { stage: 'Interest', count: 7500, rate: 75 }, + { stage: 'Consideration', count: 5000, rate: 50 }, + { stage: 'Intent', count: 3000, rate: 30 }, + { stage: 'Evaluation', count: 2000, rate: 20 }, + { stage: 'Purchase', count: 1000, rate: 10 } + ]); }; fetchAnalyticsData(); @@ -562,12 +679,12 @@ const Analytics: React.FC = () => { } } else { console.error('API返回的数据格式不正确:', result); - setFilteredEngagementData([]); + setFilteredEngagementData([]); } - } else { + } else { console.error('API调用失败:', result.error || '未知错误'); - setFilteredEngagementData([]); - } + setFilteredEngagementData([]); + } } else { console.error('获取贴文表现数据失败,HTTP状态:', response.status); const errorText = await response.text(); @@ -888,7 +1005,7 @@ const Analytics: React.FC = () => { setTimeRange(e.target.value)} + onChange={(e) => handleTimeRangeChange(e.target.value)} > @@ -1377,7 +1529,7 @@ const Analytics: React.FC = () => {