import { createLogger } from '../logger' const log = createLogger('ReportAnalyzer') /** * Parsed report data structure */ export interface ParsedReportData { /** 报告日期 (从文件名或内容提取) */ date: string /** 操作用户 */ username: string /** 处理订单数 */ ordersProcessed: number /** 删除物料数 */ materialsDeleted: number /** 跳过物料数 */ materialsSkipped: number /** 错误数量 */ errorCount: number /** 重试订单数 */ retriedOrders: number /** 成功重试数 */ successfulRetries: number /** 执行耗时 (秒) */ durationSeconds: number } /** * ReportAnalyzer - 解析和分析 ERP 物料清理执行报告 */ export class ReportAnalyzer { /** * 解析单个 markdown 报告内容 * @param content markdown 报告内容 * @returns 解析后的结构化数据,解析失败返回 null */ parseMarkdownReport(content: string): ParsedReportData | null { try { // 从执行摘要表格中提取数据 - 捕获整个表格直到空行或下一个标题 const summarySectionMatch = content.match(/## 执行摘要\s*\n([\s\S]*?)(?=\n---|\n##|\n$)/) if (!summarySectionMatch) { log.warn('Failed to find execution summary section') return null } const summaryTable = summarySectionMatch[0] // 提取各个字段的值 const username = this.extractFieldValue(summaryTable, '操作用户') || '' const ordersProcessed = this.extractNumericField(summaryTable, '处理订单数') || 0 const materialsDeleted = this.extractNumericField(summaryTable, '删除物料数') || 0 const materialsSkipped = this.extractNumericField(summaryTable, '跳过物料数') || 0 const errorCount = this.extractNumericField(summaryTable, '错误数量') || 0 const retriedOrders = this.extractNumericField(summaryTable, '重试订单数') || 0 const successfulRetries = this.extractNumericField(summaryTable, '成功重试数') || 0 // 提取执行耗时并转换为秒 const durationStr = this.extractFieldValue(summaryTable, '执行耗时') const durationSeconds = durationStr ? this.parseDuration(durationStr) : 0 // 从内容或执行时间字段提取日期 const executionTimeStr = this.extractFieldValue(summaryTable, '执行时间') const date = executionTimeStr ? this.extractDateFromDateTime(executionTimeStr) : this.extractDateFromContent(content) return { date, username, ordersProcessed, materialsDeleted, materialsSkipped, errorCount, retriedOrders, successfulRetries, durationSeconds } } catch (error) { log.error('Failed to parse markdown report', { error: error instanceof Error ? error.message : error }) return null } } /** * 批量解析多个报告 * @param reports 报告元数据和内容列表 * @returns 解析后的数据列表 (自动跳过解析失败的报告) */ analyzeReports(reports: Array<{ content: string; filename?: string }>): ParsedReportData[] { const results: 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 } } results.push(parsed) } else { log.warn('Skipping report due to parse failure', { filename: report.filename }) } } return results } /** * 从表格行中提取字段的字符串值 */ private extractFieldValue(tableContent: string, fieldName: string): string | null { // 匹配格式:| **字段名** | `值` | const pattern = new RegExp( `\\|\\s*\\*\\*${this.escapeRegex(fieldName)}\\*\\*\\s*\\|\\s*\`([^\`]*)\``, 'i' ) const match = tableContent.match(pattern) log.debug('extractFieldValue', { fieldName, matched: match?.[1], pattern }) return match ? match[1].trim() : null } /** * 从表格行中提取数值字段 */ private extractNumericField(tableContent: string, fieldName: string): number | null { const value = this.extractFieldValue(tableContent, fieldName) if (value === null) return null // 移除可能的非数字字符 (如单位) const numStr = value.replace(/[^\d]/g, '') if (!numStr) return null return parseInt(numStr, 10) } /** * 解析耗时字符串为秒数 * 支持格式:"2 分 30 秒", "1 分", "30 秒", "5 分钟" */ private parseDuration(durationStr: string): number { let totalSeconds = 0 // 匹配分钟 - 支持 "X 分" 或 "X 分钟" 格式 const minMatch = durationStr.match(/(\d+) 分/) if (minMatch) { totalSeconds += parseInt(minMatch[1], 10) * 60 } // 匹配秒 const secMatch = durationStr.match(/(\d+) 秒/) if (secMatch) { totalSeconds += parseInt(secMatch[1], 10) } return totalSeconds } /** * 从日期时间字符串中提取日期部分 * 格式:"2026-03-25 08:30:45" -> "2026-03-25" */ private extractDateFromDateTime(dateTimeStr: string): string { const match = dateTimeStr.match(/(\d{4}-\d{2}-\d{2})/) return match ? match[1] : dateTimeStr } /** * 从内容中提取日期 (备选方案) */ private extractDateFromContent(content: string): string { // 尝试从报告标题或执行时间提取 const dateMatch = content.match(/(\d{4}-\d{2}-\d{2})/) return dateMatch ? dateMatch[1] : new Date().toISOString().split('T')[0] } /** * 从文件名中提取日期 * 格式:"cleaner-report-2026-03-25-08-30-45.md" -> "2026-03-25" */ private extractDateFromFilename(filename: string): string | null { // 匹配 ISO 日期格式 const isoMatch = filename.match(/(\d{4}-\d{2}-\d{2})/) if (isoMatch) { return isoMatch[1] } // 匹配其他常见日期格式 const dateMatch = filename.match(/(\d{8})/) if (dateMatch) { const str = dateMatch[1] // 尝试解析 YYYYMMDD if (str.length === 8) { return `${str.slice(0, 4)}-${str.slice(4, 6)}-${str.slice(6, 8)}` } } return null } /** * 转义正则表达式特殊字符 */ private escapeRegex(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } }