package pdf import ( "context" "encoding/json" "fmt" "regexp" "strings" "tyapi-server/internal/domains/product/entities" "go.uber.org/zap" ) // DatabaseTableReader 数据库表格数据读取器 type DatabaseTableReader struct { logger *zap.Logger } // NewDatabaseTableReader 创建数据库表格数据读取器 func NewDatabaseTableReader(logger *zap.Logger) *DatabaseTableReader { return &DatabaseTableReader{ logger: logger, } } // TableData 表格数据 type TableData struct { Headers []string Rows [][]string } // TableWithTitle 带标题的表格 type TableWithTitle struct { Title string // 表格标题(markdown标题) Table *TableData // 表格数据 } // ReadTableFromDocumentation 从产品文档中读取表格数据 // 先将markdown表格转换为JSON格式,然后再转换为表格数据 func (r *DatabaseTableReader) ReadTableFromDocumentation(ctx context.Context, doc *entities.ProductDocumentation, fieldType string) (*TableData, error) { var content string switch fieldType { case "request_params": content = doc.RequestParams case "response_fields": content = doc.ResponseFields case "response_example": content = doc.ResponseExample case "error_codes": content = doc.ErrorCodes default: return nil, fmt.Errorf("未知的字段类型: %s", fieldType) } // 检查内容是否为空(去除空白字符后) trimmedContent := strings.TrimSpace(content) if trimmedContent == "" { return nil, fmt.Errorf("字段 %s 内容为空", fieldType) } // 先尝试解析为JSON数组(如果已经是JSON格式) var jsonArray []map[string]interface{} if err := json.Unmarshal([]byte(content), &jsonArray); err == nil && len(jsonArray) > 0 { r.logger.Info("数据已经是JSON格式,直接使用", zap.String("field_type", fieldType), zap.Int("json_array_length", len(jsonArray))) return r.convertJSONArrayToTable(jsonArray), nil } // 尝试解析为单个JSON对象(包含数组字段) var jsonObj map[string]interface{} if err := json.Unmarshal([]byte(content), &jsonObj); err == nil { // 查找包含数组的字段 for _, value := range jsonObj { if arr, ok := value.([]interface{}); ok && len(arr) > 0 { // 转换为map数组 mapArray := make([]map[string]interface{}, 0, len(arr)) for _, item := range arr { if itemMap, ok := item.(map[string]interface{}); ok { mapArray = append(mapArray, itemMap) } } if len(mapArray) > 0 { r.logger.Info("从JSON对象中提取数组数据", zap.String("field_type", fieldType)) return r.convertJSONArrayToTable(mapArray), nil } } } } // 如果不是JSON格式,先解析为markdown表格,然后转换为JSON格式 r.logger.Info("开始解析markdown表格并转换为JSON", zap.String("field_type", fieldType), zap.Int("content_length", len(content)), zap.String("content_preview", r.getContentPreview(content, 200))) tableData, err := r.parseMarkdownTable(content) if err != nil { r.logger.Error("解析markdown表格失败", zap.String("field_type", fieldType), zap.Error(err), zap.String("content_preview", r.getContentPreview(content, 500))) return nil, fmt.Errorf("解析markdown表格失败: %w", err) } r.logger.Info("markdown表格解析成功", zap.String("field_type", fieldType), zap.Int("header_count", len(tableData.Headers)), zap.Int("row_count", len(tableData.Rows)), zap.Strings("headers", tableData.Headers)) // 将markdown表格数据转换为JSON格式(保持列顺序) r.logger.Debug("开始将表格数据转换为JSON格式", zap.String("field_type", fieldType)) jsonArray = r.convertTableDataToJSON(tableData) r.logger.Info("表格数据已转换为JSON格式", zap.String("field_type", fieldType), zap.Int("json_array_length", len(jsonArray))) // 记录转换后的JSON(用于调试) jsonBytes, marshalErr := json.MarshalIndent(jsonArray, "", " ") if marshalErr != nil { r.logger.Warn("JSON序列化失败", zap.String("field_type", fieldType), zap.Error(marshalErr)) } else { previewLen := len(jsonBytes) if previewLen > 1000 { previewLen = 1000 } r.logger.Debug("转换后的JSON数据预览", zap.String("field_type", fieldType), zap.Int("json_length", len(jsonBytes)), zap.String("json_preview", string(jsonBytes[:previewLen]))) // 如果JSON数据较大,记录完整路径提示 if len(jsonBytes) > 1000 { r.logger.Info("JSON数据较大,完整内容请查看debug级别日志", zap.String("field_type", fieldType), zap.Int("json_length", len(jsonBytes))) } } // 将JSON数据转换回表格数据用于渲染(使用原始表头顺序保持列顺序) return r.convertJSONArrayToTableWithOrder(jsonArray, tableData.Headers), nil } // convertJSONArrayToTable 将JSON数组转换为表格数据(用于已经是JSON格式的数据) func (r *DatabaseTableReader) convertJSONArrayToTable(data []map[string]interface{}) *TableData { if len(data) == 0 { return &TableData{ Headers: []string{}, Rows: [][]string{}, } } // 收集所有列名(按第一次出现的顺序) columnSet := make(map[string]bool) columns := make([]string, 0) // 从第一行开始收集列名,保持第一次出现的顺序 for _, row := range data { for key := range row { if !columnSet[key] { columns = append(columns, key) columnSet[key] = true } } // 只从第一行收集,保持顺序 if len(columns) > 0 { break } } // 如果第一行没有收集到所有列,继续收集(但顺序可能不稳定) if len(columns) == 0 { for _, row := range data { for key := range row { if !columnSet[key] { columns = append(columns, key) columnSet[key] = true } } } } // 构建表头 headers := make([]string, len(columns)) copy(headers, columns) // 构建数据行 rows := make([][]string, 0, len(data)) for _, row := range data { rowData := make([]string, len(columns)) for i, col := range columns { value := row[col] rowData[i] = r.formatValue(value) } rows = append(rows, rowData) } return &TableData{ Headers: headers, Rows: rows, } } // convertJSONArrayToTableWithOrder 将JSON数组转换为表格数据(使用指定的列顺序) func (r *DatabaseTableReader) convertJSONArrayToTableWithOrder(data []map[string]interface{}, originalHeaders []string) *TableData { if len(data) == 0 { return &TableData{ Headers: originalHeaders, Rows: [][]string{}, } } // 使用原始表头顺序 headers := make([]string, len(originalHeaders)) copy(headers, originalHeaders) // 构建数据行,按照原始表头顺序 rows := make([][]string, 0, len(data)) for _, row := range data { rowData := make([]string, len(headers)) for i, header := range headers { value := row[header] rowData[i] = r.formatValue(value) } rows = append(rows, rowData) } r.logger.Debug("JSON转表格完成(保持列顺序)", zap.Int("header_count", len(headers)), zap.Int("row_count", len(rows)), zap.Strings("headers", headers)) return &TableData{ Headers: headers, Rows: rows, } } // parseMarkdownTablesWithTitles 解析markdown格式的表格(支持多个表格,保留标题) func (r *DatabaseTableReader) parseMarkdownTablesWithTitles(content string) ([]TableWithTitle, error) { lines := strings.Split(content, "\n") var result []TableWithTitle var currentTitle string var currentHeaders []string var currentRows [][]string inTable := false hasValidHeader := false nonTableLineCount := 0 maxNonTableLines := 3 // 允许最多3个连续非表格行 for _, line := range lines { line = strings.TrimSpace(line) // 处理markdown标题行(以#开头)- 保存标题 if strings.HasPrefix(line, "#") { // 如果当前有表格,先保存 if inTable && len(currentHeaders) > 0 { result = append(result, TableWithTitle{ Title: currentTitle, Table: &TableData{ Headers: currentHeaders, Rows: currentRows, }, }) currentHeaders = nil currentRows = nil inTable = false hasValidHeader = false } // 提取标题(移除#和空格) currentTitle = strings.TrimSpace(strings.TrimPrefix(line, "#")) currentTitle = strings.TrimSpace(strings.TrimPrefix(currentTitle, "#")) currentTitle = strings.TrimSpace(strings.TrimPrefix(currentTitle, "#")) nonTableLineCount = 0 continue } // 跳过空行 if line == "" { if inTable { nonTableLineCount++ if nonTableLineCount > maxNonTableLines { // 当前表格结束,保存并重置 if len(currentHeaders) > 0 { result = append(result, TableWithTitle{ Title: currentTitle, Table: &TableData{ Headers: currentHeaders, Rows: currentRows, }, }) currentHeaders = nil currentRows = nil currentTitle = "" } inTable = false hasValidHeader = false nonTableLineCount = 0 } } continue } // 检查是否是markdown表格行 if !strings.Contains(line, "|") { // 如果已经在表格中,遇到非表格行则计数 if inTable { nonTableLineCount++ // 如果连续非表格行过多,表格结束 if nonTableLineCount > maxNonTableLines { // 当前表格结束,保存并重置 if len(currentHeaders) > 0 { result = append(result, TableWithTitle{ Title: currentTitle, Table: &TableData{ Headers: currentHeaders, Rows: currentRows, }, }) currentHeaders = nil currentRows = nil currentTitle = "" } inTable = false hasValidHeader = false nonTableLineCount = 0 } } continue } // 重置非表格行计数(遇到表格行了) nonTableLineCount = 0 // 跳过分隔行 if r.isSeparatorLine(line) { // 分隔行后应该开始数据行 if hasValidHeader { continue } // 如果还没有表头,跳过分隔行 continue } // 解析表格行 cells := strings.Split(line, "|") // 清理首尾空元素 if len(cells) > 0 && strings.TrimSpace(cells[0]) == "" { cells = cells[1:] } if len(cells) > 0 && strings.TrimSpace(cells[len(cells)-1]) == "" { cells = cells[:len(cells)-1] } // 清理每个单元格,过滤空字符 cleanedCells := make([]string, 0, len(cells)) for _, cell := range cells { cleaned := strings.TrimSpace(cell) // 移除HTML标签(如
) cleaned = r.removeHTMLTags(cleaned) cleanedCells = append(cleanedCells, cleaned) } // 检查这一行是否有有效内容 hasContent := false for _, cell := range cleanedCells { if strings.TrimSpace(cell) != "" { hasContent = true break } } if !hasContent || len(cleanedCells) == 0 { continue } if !inTable { // 第一行作为表头 currentHeaders = cleanedCells inTable = true hasValidHeader = true } else { // 数据行,确保列数与表头一致 row := make([]string, len(currentHeaders)) for i := range row { if i < len(cleanedCells) { row[i] = cleanedCells[i] } else { row[i] = "" } } // 检查数据行是否有有效内容(至少有一个非空单元格) hasData := false for _, cell := range row { if strings.TrimSpace(cell) != "" { hasData = true break } } // 只添加有有效内容的数据行 if hasData { currentRows = append(currentRows, row) } } } // 处理最后一个表格 if len(currentHeaders) > 0 { result = append(result, TableWithTitle{ Title: currentTitle, Table: &TableData{ Headers: currentHeaders, Rows: currentRows, }, }) } if len(result) == 0 { return nil, fmt.Errorf("无法解析表格:未找到表头") } r.logger.Info("解析多个表格完成", zap.Int("table_count", len(result))) return result, nil } // parseMarkdownTable 解析markdown格式的表格(兼容方法,调用新方法) func (r *DatabaseTableReader) parseMarkdownTable(content string) (*TableData, error) { tablesWithTitles, err := r.parseMarkdownTablesWithTitles(content) if err != nil { return nil, err } if len(tablesWithTitles) == 0 { return nil, fmt.Errorf("未找到任何表格") } // 返回第一个表格(向后兼容) return tablesWithTitles[0].Table, nil } // mergeTables 合并多个表格(使用最宽的表头) func (r *DatabaseTableReader) mergeTables(existingHeaders []string, existingRows [][]string, newHeaders []string, newRows [][]string) ([]string, [][]string) { // 如果这是第一个表格,直接返回 if len(existingHeaders) == 0 { return newHeaders, newRows } // 使用最宽的表头(列数最多的) var finalHeaders []string if len(newHeaders) > len(existingHeaders) { finalHeaders = make([]string, len(newHeaders)) copy(finalHeaders, newHeaders) } else { finalHeaders = make([]string, len(existingHeaders)) copy(finalHeaders, existingHeaders) } // 合并所有行,确保列数与最终表头一致 mergedRows := make([][]string, 0, len(existingRows)+len(newRows)) // 添加已有行 for _, row := range existingRows { adjustedRow := make([]string, len(finalHeaders)) copy(adjustedRow, row) mergedRows = append(mergedRows, adjustedRow) } // 添加新行 for _, row := range newRows { adjustedRow := make([]string, len(finalHeaders)) for i := range adjustedRow { if i < len(row) { adjustedRow[i] = row[i] } else { adjustedRow[i] = "" } } mergedRows = append(mergedRows, adjustedRow) } return finalHeaders, mergedRows } // removeHTMLTags 移除HTML标签(如
)和样式信息 func (r *DatabaseTableReader) removeHTMLTags(text string) string { // 先移除所有HTML标签(包括带样式的标签,如 ) // 使用正则表达式移除所有HTML标签及其内容 re := regexp.MustCompile(`<[^>]+>`) text = re.ReplaceAllString(text, "") // 替换常见的HTML换行标签为空格 text = strings.ReplaceAll(text, "
", " ") text = strings.ReplaceAll(text, "
", " ") text = strings.ReplaceAll(text, "
", " ") text = strings.ReplaceAll(text, "\n", " ") // 移除HTML实体 text = strings.ReplaceAll(text, " ", " ") text = strings.ReplaceAll(text, "&", "&") text = strings.ReplaceAll(text, "<", "<") text = strings.ReplaceAll(text, ">", ">") text = strings.ReplaceAll(text, """, "\"") text = strings.ReplaceAll(text, "'", "'") return strings.TrimSpace(text) } // isSeparatorLine 检查是否是markdown表格的分隔行 func (r *DatabaseTableReader) isSeparatorLine(line string) bool { if !strings.Contains(line, "-") { return false } for _, r := range line { if r != '|' && r != '-' && r != ':' && r != ' ' { return false } } return true } // convertTableDataToJSON 将表格数据转换为JSON数组格式 func (r *DatabaseTableReader) convertTableDataToJSON(tableData *TableData) []map[string]interface{} { if tableData == nil || len(tableData.Headers) == 0 { r.logger.Warn("表格数据为空,无法转换为JSON") return []map[string]interface{}{} } jsonArray := make([]map[string]interface{}, 0, len(tableData.Rows)) validRowCount := 0 for rowIndex, row := range tableData.Rows { rowObj := make(map[string]interface{}) for i, header := range tableData.Headers { // 获取对应的单元格值 var cellValue string if i < len(row) { cellValue = strings.TrimSpace(row[i]) } // 将表头作为key,单元格值作为value header = strings.TrimSpace(header) if header != "" { rowObj[header] = cellValue } } // 只添加有有效数据的行 if len(rowObj) > 0 { jsonArray = append(jsonArray, rowObj) validRowCount++ } else { r.logger.Debug("跳过空行", zap.Int("row_index", rowIndex)) } } r.logger.Debug("表格转JSON完成", zap.Int("total_rows", len(tableData.Rows)), zap.Int("valid_rows", validRowCount), zap.Int("json_array_length", len(jsonArray))) return jsonArray } // getContentPreview 获取内容预览(用于日志记录) func (r *DatabaseTableReader) getContentPreview(content string, maxLen int) string { content = strings.TrimSpace(content) if len(content) <= maxLen { return content } return content[:maxLen] + "..." } // formatValue 格式化值为字符串 func (r *DatabaseTableReader) formatValue(value interface{}) string { if value == nil { return "" } var result string switch v := value.(type) { case string: result = strings.TrimSpace(v) // 如果去除空白后为空,返回空字符串 if result == "" { return "" } // 移除HTML标签和样式,确保数据干净 result = r.removeHTMLTags(result) return result case bool: if v { return "是" } return "否" case float64: if v == float64(int64(v)) { return fmt.Sprintf("%.0f", v) } return fmt.Sprintf("%g", v) case int, int8, int16, int32, int64: return fmt.Sprintf("%d", v) case uint, uint8, uint16, uint32, uint64: return fmt.Sprintf("%d", v) default: result = fmt.Sprintf("%v", v) // 去除空白字符 result = strings.TrimSpace(result) if result == "" { return "" } return result } }