morte change

This commit is contained in:
2025-03-14 21:10:50 +08:00
parent 5a03323c69
commit 942fb592b5
6 changed files with 802 additions and 271 deletions

View File

@@ -56,7 +56,7 @@
- 条形长度直观反映各平台占比
- 帮助团队了解哪些平台效果更好
# 审核状态分布 [先不做]
# 审核状态分布 [已实现]
- 环形图展示内容审核状态的分布情况
- 包括三种状态:已核准、待审核、已拒绝

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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,31 +3036,142 @@ 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'
}
}
}
}

View File

@@ -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<string | null>(null);
const [maxTimelineCount, setMaxTimelineCount] = useState(1); // 设置默认值为1避免除以零
// 添加审核状态分布相关状态
const [moderationLoading, setModerationLoading] = useState(true);
const [moderationError, setModerationError] = useState<string | null>(null);
// 添加项目相关状态
const [projects, setProjects] = useState<Project[]>([
{ id: '1', name: '项目 1', description: '示例项目 1' },
@@ -290,6 +313,102 @@ const Analytics: React.FC = () => {
const [postsLoading, setPostsLoading] = useState(true);
const [postsError, setPostsError] = useState<string | null>(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 {
@@ -888,7 +1005,7 @@ const Analytics: React.FC = () => {
<select
id="project-select"
value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)}
onChange={(e) => handleProjectChange(e.target.value)}
className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
<option value="all"></option>
@@ -1323,6 +1440,41 @@ const Analytics: React.FC = () => {
}
};
const handleTimeRangeChange = (range: string) => {
setTimeRange(range);
// 后续刷新数据
fetchDashboardCards();
fetchCommentTrend();
fetchPlatformDistribution();
fetchSentimentAnalysis();
fetchPopularPosts();
fetchModerationStatus(); // 添加刷新审核状态数据
};
// 项目选择变化处理函数
const handleProjectChange = (projectId: string) => {
setSelectedProject(projectId);
// 刷新数据
fetchDashboardCards();
fetchCommentTrend();
fetchPlatformDistribution();
fetchSentimentAnalysis();
fetchPopularPosts();
fetchModerationStatus(); // 添加刷新审核状态数据
};
// 平台选择变化处理函数
const handlePlatformChange = (platform: string) => {
setSelectedPlatform(platform);
// 刷新数据
fetchDashboardCards();
fetchCommentTrend();
fetchPlatformDistribution();
fetchSentimentAnalysis();
fetchPopularPosts();
fetchModerationStatus(); // 添加刷新审核状态数据
};
return (
<div className="flex-1 overflow-auto">
<div className="p-6">
@@ -1356,7 +1508,7 @@ const Analytics: React.FC = () => {
<select
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
onChange={(e) => handleTimeRangeChange(e.target.value)}
>
<option value="7"> 7 </option>
<option value="30"> 30 </option>
@@ -1377,7 +1529,7 @@ const Analytics: React.FC = () => {
<select
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={selectedPlatform}
onChange={(e) => setSelectedPlatform(e.target.value)}
onChange={(e) => handlePlatformChange(e.target.value)}
>
<option value="all"></option>
<option value="facebook">Facebook</option>
@@ -1977,14 +2129,31 @@ const Analytics: React.FC = () => {
</div>
{/* 审核状态分布 */}
{/* <div className="p-6 bg-white rounded-lg shadow">
<div className="p-6 bg-white rounded-lg shadow">
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
{moderationLoading ? (
<div className="flex flex-col items-center justify-center h-64">
<div className="w-12 h-12 border-4 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
<p className="mt-4 text-gray-600">...</p>
</div>
) : moderationError ? (
<div className="flex flex-col items-center justify-center h-64 p-4 rounded-lg bg-red-50">
<AlertTriangle className="w-10 h-10 mb-2 text-red-500" />
<p className="text-center text-red-600">{moderationError}</p>
</div>
) : statusData.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 p-4 rounded-lg bg-gray-50">
<AlertTriangle className="w-10 h-10 mb-2 text-gray-400" />
<p className="text-center text-gray-600"></p>
</div>
) : (
<>
<div className="flex justify-center mb-6">
<div className="relative w-48 h-48 rounded-full">
{statusData.map((item, index) => {
// 计算每个扇形的起始角度和结束角度
const startAngle = index === 0 ? 0 : statusData.slice(0, index).reduce((sum, i) => sum + i.percentage, 0) * 3.6;
const endAngle = startAngle + item.percentage * 3.6;
const startAngle = index === 0 ? 0 : statusData.slice(0, index).reduce((sum, i) => sum + (i.percentage || 0), 0) * 3.6;
const endAngle = startAngle + (item.percentage || 0) * 3.6;
return (
<div
@@ -2011,12 +2180,14 @@ const Analytics: React.FC = () => {
</div>
<div className="flex items-center">
<span className="mr-2 text-sm text-gray-500">{item.value} </span>
<span className="text-sm font-medium text-gray-700">{item.percentage}%</span>
<span className="text-sm font-medium text-gray-700">{item.percentage?.toFixed(1)}%</span>
</div>
</div>
))}
</div>
</div> */}
</>
)}
</div>
</div>
{/* 情感分析详情 */}
@@ -2061,7 +2232,7 @@ const Analytics: React.FC = () => {
</div>
{/* 热门文章 */}
<div className="p-6 mt-8 bg-white rounded-lg shadow">
<div className="p-6 mt-8 bg-white rounded-lg shadow" style={{marginTop: '20px'}}>
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
{postsLoading ? (
<div className="flex flex-col items-center justify-center h-64">