morte change
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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<ModerationStatusResponse> {
|
||||
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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user