feat: report analysis with date aggregation and user filtering
- Implement date aggregation logic, sum multiple reports on same day - Add user filtering with multi-select Chip components - Fix IpcResult data unwrapping bug in frontend - Add loading state and empty state indicators - Update test cases to verify aggregation logic Fixes: - Timeline confusion: Now sorted by date ascending - Data duplication: Same-day data automatically aggregated - No filtering: Added user selector for filtering by users
This commit is contained in:
@@ -3,7 +3,7 @@ import { createLogger } from '../logger'
|
||||
const log = createLogger('ReportAnalyzer')
|
||||
|
||||
/**
|
||||
* Parsed report data structure
|
||||
* Parsed report data structure - 单条报告记录
|
||||
*/
|
||||
export interface ParsedReportData {
|
||||
/** 报告日期 (从文件名或内容提取) */
|
||||
@@ -26,6 +26,30 @@ export interface ParsedReportData {
|
||||
durationSeconds: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregated daily data - 按日期聚合后的数据
|
||||
*/
|
||||
export interface AggregatedDailyData {
|
||||
/** 日期 */
|
||||
date: string
|
||||
/** 处理订单数总和 */
|
||||
ordersProcessed: number
|
||||
/** 删除物料数总和 */
|
||||
materialsDeleted: number
|
||||
/** 跳过物料数总和 */
|
||||
materialsSkipped: number
|
||||
/** 错误数量总和 */
|
||||
errorCount: number
|
||||
/** 重试订单数总和 */
|
||||
retriedOrders: number
|
||||
/** 成功重试数总和 */
|
||||
successfulRetries: number
|
||||
/** 执行耗时平均值 (秒) */
|
||||
avgDurationSeconds: number
|
||||
/** 执行次数 */
|
||||
executionCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* ReportAnalyzer - 解析和分析 ERP 物料清理执行报告
|
||||
*/
|
||||
@@ -85,12 +109,13 @@ export class ReportAnalyzer {
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量解析多个报告
|
||||
* 批量解析多个报告并按日期聚合
|
||||
* @param reports 报告元数据和内容列表
|
||||
* @returns 解析后的数据列表 (自动跳过解析失败的报告)
|
||||
* @returns 按日期聚合后的数据,按日期升序排序
|
||||
*/
|
||||
analyzeReports(reports: Array<{ content: string; filename?: string }>): ParsedReportData[] {
|
||||
const results: ParsedReportData[] = []
|
||||
analyzeReports(reports: Array<{ content: string; filename?: string }>): AggregatedDailyData[] {
|
||||
// 第一步:解析所有报告
|
||||
const parsedReports: ParsedReportData[] = []
|
||||
|
||||
for (const report of reports) {
|
||||
const parsed = this.parseMarkdownReport(report.content)
|
||||
@@ -102,13 +127,153 @@ export class ReportAnalyzer {
|
||||
parsed.date = dateFromFilename
|
||||
}
|
||||
}
|
||||
results.push(parsed)
|
||||
parsedReports.push(parsed)
|
||||
} else {
|
||||
log.warn('Skipping report due to parse failure', { filename: report.filename })
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
// 第二步:按日期聚合数据
|
||||
const dateMap = new Map<string, ParsedReportData[]>()
|
||||
for (const report of parsedReports) {
|
||||
const existing = dateMap.get(report.date) || []
|
||||
existing.push(report)
|
||||
dateMap.set(report.date, existing)
|
||||
}
|
||||
|
||||
// 第三步:计算每天的聚合数据
|
||||
const aggregated: AggregatedDailyData[] = []
|
||||
for (const [date, reportsOnDate] of dateMap.entries()) {
|
||||
const totalOrdersProcessed = reportsOnDate.reduce((sum, r) => sum + r.ordersProcessed, 0)
|
||||
const totalMaterialsDeleted = reportsOnDate.reduce((sum, r) => sum + r.materialsDeleted, 0)
|
||||
const totalMaterialsSkipped = reportsOnDate.reduce((sum, r) => sum + r.materialsSkipped, 0)
|
||||
const totalErrorCount = reportsOnDate.reduce((sum, r) => sum + r.errorCount, 0)
|
||||
const totalRetriedOrders = reportsOnDate.reduce((sum, r) => sum + r.retriedOrders, 0)
|
||||
const totalSuccessfulRetries = reportsOnDate.reduce((sum, r) => sum + r.successfulRetries, 0)
|
||||
const totalDurationSeconds = reportsOnDate.reduce((sum, r) => sum + r.durationSeconds, 0)
|
||||
const executionCount = reportsOnDate.length
|
||||
|
||||
aggregated.push({
|
||||
date,
|
||||
ordersProcessed: totalOrdersProcessed,
|
||||
materialsDeleted: totalMaterialsDeleted,
|
||||
materialsSkipped: totalMaterialsSkipped,
|
||||
errorCount: totalErrorCount,
|
||||
retriedOrders: totalRetriedOrders,
|
||||
successfulRetries: totalSuccessfulRetries,
|
||||
avgDurationSeconds: Math.round(totalDurationSeconds / executionCount),
|
||||
executionCount
|
||||
})
|
||||
}
|
||||
|
||||
// 第四步:按日期升序排序(从早到晚),确保图表时间轴正确
|
||||
aggregated.sort((a, b) => {
|
||||
const dateA = new Date(a.date)
|
||||
const dateB = new Date(b.date)
|
||||
return dateA.getTime() - dateB.getTime()
|
||||
})
|
||||
|
||||
return aggregated
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有唯一的用户名列表
|
||||
* @param reports 报告元数据和内容列表
|
||||
* @returns 按字母顺序排序的用户名列表
|
||||
*/
|
||||
getAllUsernames(reports: Array<{ content: string; filename?: string }>): string[] {
|
||||
const usernames = new Set<string>()
|
||||
|
||||
for (const report of reports) {
|
||||
const parsed = this.parseMarkdownReport(report.content)
|
||||
if (parsed && parsed.username) {
|
||||
usernames.add(parsed.username)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(usernames).sort()
|
||||
}
|
||||
|
||||
/**
|
||||
* 按用户筛选报告并返回聚合数据
|
||||
* @param reports 报告元数据和内容列表
|
||||
* @param selectedUsernames 选中的用户名列表
|
||||
* @returns 按日期聚合后的数据,仅包含选中用户的报告
|
||||
*/
|
||||
analyzeReportsByUsers(
|
||||
reports: Array<{ content: string; filename?: string }>,
|
||||
selectedUsernames: string[]
|
||||
): AggregatedDailyData[] {
|
||||
// 如果未选择任何用户,返回空数组
|
||||
if (selectedUsernames.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 第一步:解析所有报告
|
||||
const parsedReports: ParsedReportData[] = []
|
||||
|
||||
for (const report of reports) {
|
||||
const parsed = this.parseMarkdownReport(report.content)
|
||||
if (parsed) {
|
||||
// 如果有文件名,尝试从文件名提取日期覆盖
|
||||
if (report.filename) {
|
||||
const dateFromFilename = this.extractDateFromFilename(report.filename)
|
||||
if (dateFromFilename) {
|
||||
parsed.date = dateFromFilename
|
||||
}
|
||||
}
|
||||
parsedReports.push(parsed)
|
||||
} else {
|
||||
log.warn('Skipping report due to parse failure', { filename: report.filename })
|
||||
}
|
||||
}
|
||||
|
||||
// 第二步:按选中的用户过滤
|
||||
const filteredReports = parsedReports.filter((report) =>
|
||||
selectedUsernames.includes(report.username)
|
||||
)
|
||||
|
||||
// 第三步:按日期聚合过滤后的数据
|
||||
const dateMap = new Map<string, ParsedReportData[]>()
|
||||
for (const report of filteredReports) {
|
||||
const existing = dateMap.get(report.date) || []
|
||||
existing.push(report)
|
||||
dateMap.set(report.date, existing)
|
||||
}
|
||||
|
||||
// 第四步:计算每天的聚合数据
|
||||
const aggregated: AggregatedDailyData[] = []
|
||||
for (const [date, reportsOnDate] of dateMap.entries()) {
|
||||
const totalOrdersProcessed = reportsOnDate.reduce((sum, r) => sum + r.ordersProcessed, 0)
|
||||
const totalMaterialsDeleted = reportsOnDate.reduce((sum, r) => sum + r.materialsDeleted, 0)
|
||||
const totalMaterialsSkipped = reportsOnDate.reduce((sum, r) => sum + r.materialsSkipped, 0)
|
||||
const totalErrorCount = reportsOnDate.reduce((sum, r) => sum + r.errorCount, 0)
|
||||
const totalRetriedOrders = reportsOnDate.reduce((sum, r) => sum + r.retriedOrders, 0)
|
||||
const totalSuccessfulRetries = reportsOnDate.reduce((sum, r) => sum + r.successfulRetries, 0)
|
||||
const totalDurationSeconds = reportsOnDate.reduce((sum, r) => sum + r.durationSeconds, 0)
|
||||
const executionCount = reportsOnDate.length
|
||||
|
||||
aggregated.push({
|
||||
date,
|
||||
ordersProcessed: totalOrdersProcessed,
|
||||
materialsDeleted: totalMaterialsDeleted,
|
||||
materialsSkipped: totalMaterialsSkipped,
|
||||
errorCount: totalErrorCount,
|
||||
retriedOrders: totalRetriedOrders,
|
||||
successfulRetries: totalSuccessfulRetries,
|
||||
avgDurationSeconds: Math.round(totalDurationSeconds / executionCount),
|
||||
executionCount
|
||||
})
|
||||
}
|
||||
|
||||
// 第五步:按日期升序排序(从早到晚),确保图表时间轴正确
|
||||
aggregated.sort((a, b) => {
|
||||
const dateA = new Date(a.date)
|
||||
const dateB = new Date(b.date)
|
||||
return dateA.getTime() - dateB.getTime()
|
||||
})
|
||||
|
||||
return aggregated
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user