From 2c89b8cb26d50e990e585b2f9f46b3f8abdaf85d Mon Sep 17 00:00:00 2001 From: 18278715334 <18278715334@163.com> Date: Thu, 11 Dec 2025 11:14:31 +0800 Subject: [PATCH] =?UTF-8?q?fix=E7=BB=84=E5=90=88=E5=8C=85=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../http/handlers/product_handler.go | 118 +++++- internal/shared/pdf/page_builder.go | 368 +++++++++++++++++- .../shared/pdf/pdf_generator_refactored.go | 69 +++- 3 files changed, 535 insertions(+), 20 deletions(-) diff --git a/internal/infrastructure/http/handlers/product_handler.go b/internal/infrastructure/http/handlers/product_handler.go index e7416f9..5a87f74 100644 --- a/internal/infrastructure/http/handlers/product_handler.go +++ b/internal/infrastructure/http/handlers/product_handler.go @@ -15,6 +15,7 @@ import ( "tyapi-server/internal/shared/pdf" "github.com/gin-gonic/gin" + "github.com/shopspring/decimal" "go.uber.org/zap" ) @@ -765,6 +766,60 @@ func (h *ProductHandler) DownloadProductDocumentation(c *gin.Context) { docVersion = "1.0" } + // 如果是组合包,获取子产品的文档信息 + var subProductDocs []*entities.ProductDocumentation + if product.IsPackage && len(product.PackageItems) > 0 { + h.logger.Info("检测到组合包,开始获取子产品文档", + zap.String("product_id", productID), + zap.Int("sub_product_count", len(product.PackageItems)), + ) + + // 收集所有子产品的ID + subProductIDs := make([]string, 0, len(product.PackageItems)) + for _, item := range product.PackageItems { + subProductIDs = append(subProductIDs, item.ProductID) + } + + // 批量获取子产品的文档 + subDocs, err := h.documentationAppService.GetDocumentationsByProductIDs(c.Request.Context(), subProductIDs) + if err != nil { + h.logger.Warn("获取组合包子产品文档失败", + zap.String("product_id", productID), + zap.Error(err), + ) + } else { + // 转换为entity类型,并按PackageItems的顺序排序 + docMap := make(map[string]*entities.ProductDocumentation) + for i := range subDocs { + docMap[subDocs[i].ProductID] = &entities.ProductDocumentation{ + ID: subDocs[i].ID, + ProductID: subDocs[i].ProductID, + RequestURL: subDocs[i].RequestURL, + RequestMethod: subDocs[i].RequestMethod, + BasicInfo: subDocs[i].BasicInfo, + RequestParams: subDocs[i].RequestParams, + ResponseFields: subDocs[i].ResponseFields, + ResponseExample: subDocs[i].ResponseExample, + ErrorCodes: subDocs[i].ErrorCodes, + Version: subDocs[i].Version, + } + } + + // 按PackageItems的顺序构建子产品文档列表 + for _, item := range product.PackageItems { + if subDoc, exists := docMap[item.ProductID]; exists { + subProductDocs = append(subProductDocs, subDoc) + } + } + + h.logger.Info("成功获取组合包子产品文档", + zap.String("product_id", productID), + zap.Int("total_sub_products", len(product.PackageItems)), + zap.Int("docs_found", len(subProductDocs)), + ) + } + } + // 尝试从缓存获取PDF var pdfBytes []byte var cacheHit bool @@ -829,18 +884,61 @@ func (h *ProductHandler) DownloadProductDocumentation(c *gin.Context) { } }() + // 构建Product实体(用于PDF生成) + productEntity := &entities.Product{ + ID: product.ID, + Name: product.Name, + Code: product.Code, + Description: product.Description, + Content: product.Content, + IsPackage: product.IsPackage, + Price: decimal.NewFromFloat(product.Price), + } + + // 如果是组合包,添加子产品信息 + if product.IsPackage && len(product.PackageItems) > 0 { + productEntity.PackageItems = make([]*entities.ProductPackageItem, len(product.PackageItems)) + for i, item := range product.PackageItems { + productEntity.PackageItems[i] = &entities.ProductPackageItem{ + ID: item.ID, + PackageID: product.ID, + ProductID: item.ProductID, + SortOrder: item.SortOrder, + Product: &entities.Product{ + ID: item.ProductID, + Code: item.ProductCode, + Name: item.ProductName, + }, + } + } + } + // 直接调用PDF生成器(简化版本,不使用goroutine) - h.logger.Info("开始调用PDF生成器") - pdfBytes, genErr := h.pdfGenerator.GenerateProductPDF( - c.Request.Context(), - product.ID, - product.Name, - product.Code, - product.Description, - product.Content, - product.Price, - docEntity, + h.logger.Info("开始调用PDF生成器", + zap.Bool("is_package", product.IsPackage), + zap.Int("sub_product_docs_count", len(subProductDocs)), ) + + // 使用重构后的生成器 + refactoredGen := pdf.NewPDFGeneratorRefactored(h.logger) + var genErr error + + if product.IsPackage && len(subProductDocs) > 0 { + // 组合包:使用支持子产品文档的方法 + pdfBytes, genErr = refactoredGen.GenerateProductPDFWithSubProducts( + c.Request.Context(), + productEntity, + docEntity, + subProductDocs, + ) + } else { + // 普通产品:使用标准方法 + pdfBytes, genErr = refactoredGen.GenerateProductPDFFromEntity( + c.Request.Context(), + productEntity, + docEntity, + ) + } h.logger.Info("PDF生成器调用返回", zap.String("product_id", productID), zap.Bool("has_error", genErr != nil), diff --git a/internal/shared/pdf/page_builder.go b/internal/shared/pdf/page_builder.go index 2cc5d19..0cb061b 100644 --- a/internal/shared/pdf/page_builder.go +++ b/internal/shared/pdf/page_builder.go @@ -305,6 +305,325 @@ func (pb *PageBuilder) AddDocumentationPages(pdf *gofpdf.Fpdf, doc *entities.Pro pb.addAdditionalInfo(pdf, doc, chineseFontAvailable) } +// AddDocumentationPagesWithoutAdditionalInfo 添加接口文档页面(不包含二维码和说明) +// 用于组合包场景,在所有文档渲染完成后统一添加二维码和说明 +func (pb *PageBuilder) AddDocumentationPagesWithoutAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) { + // 创建自定义的AddPage函数,确保每页都有水印 + addPageWithWatermark := func() { + pdf.AddPage() + pb.addHeader(pdf, chineseFontAvailable) + pb.addWatermark(pdf, chineseFontAvailable) // 每页都添加水印 + } + + addPageWithWatermark() + + pdf.SetY(45) + pb.fontManager.SetFont(pdf, "B", 18) + _, lineHt := pdf.GetFontSize() + pdf.CellFormat(0, lineHt, "接口文档", "", 1, "L", false, 0, "") + + // 请求URL + pdf.Ln(8) + pdf.SetTextColor(0, 0, 0) // 确保深黑色 + pb.fontManager.SetFont(pdf, "B", 12) + pdf.CellFormat(0, lineHt, "请求URL:", "", 1, "L", false, 0, "") + // URL使用黑体字体(可能包含中文字符) + // 先清理URL中的乱码 + cleanURL := pb.textProcessor.CleanText(doc.RequestURL) + pb.fontManager.SetFont(pdf, "", 10) // 使用黑体 + pdf.SetTextColor(0, 0, 0) + pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false) + + // 请求方法 + pdf.Ln(5) + pb.fontManager.SetFont(pdf, "B", 12) + pdf.CellFormat(0, lineHt, fmt.Sprintf("请求方法:%s", doc.RequestMethod), "", 1, "L", false, 0, "") + + // 基本信息 + if doc.BasicInfo != "" { + pdf.Ln(8) + pb.addSection(pdf, "基本信息", doc.BasicInfo, chineseFontAvailable) + } + + // 请求参数 + if doc.RequestParams != "" { + pdf.Ln(8) + // 显示标题 + pdf.SetTextColor(0, 0, 0) + pb.fontManager.SetFont(pdf, "B", 14) + _, lineHt = pdf.GetFontSize() + pdf.CellFormat(0, lineHt, "请求参数:", "", 1, "L", false, 0, "") + + // 使用新的数据库驱动方式处理请求参数 + if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "request_params"); err != nil { + pb.logger.Warn("渲染请求参数表格失败,回退到文本显示", zap.Error(err)) + // 如果表格渲染失败,显示为文本 + text := pb.textProcessor.CleanText(doc.RequestParams) + if strings.TrimSpace(text) != "" { + pb.fontManager.SetFont(pdf, "", 10) + pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) + } + } + + // 生成JSON示例 + if jsonExample := pb.jsonProcessor.GenerateJSONExample(doc.RequestParams, pb.tableParser); jsonExample != "" { + pdf.Ln(5) + pdf.SetTextColor(0, 0, 0) // 确保深黑色 + pb.fontManager.SetFont(pdf, "B", 14) + pdf.CellFormat(0, lineHt, "请求示例:", "", 1, "L", false, 0, "") + // JSON中可能包含中文值,使用黑体字体 + pb.fontManager.SetFont(pdf, "", 9) // 使用黑体显示JSON(支持中文) + pdf.SetTextColor(0, 0, 0) + pdf.MultiCell(0, lineHt*1.3, jsonExample, "", "L", false) + } + } + + // 响应示例 + if doc.ResponseExample != "" { + pdf.Ln(8) + pdf.SetTextColor(0, 0, 0) // 确保深黑色 + pb.fontManager.SetFont(pdf, "B", 14) + _, lineHt = pdf.GetFontSize() + pdf.CellFormat(0, lineHt, "响应示例:", "", 1, "L", false, 0, "") + + // 优先尝试提取和格式化JSON + jsonContent := pb.jsonProcessor.ExtractJSON(doc.ResponseExample) + if jsonContent != "" { + // 格式化JSON + formattedJSON, err := pb.jsonProcessor.FormatJSON(jsonContent) + if err == nil { + jsonContent = formattedJSON + } + pdf.SetTextColor(0, 0, 0) // 确保深黑色 + pb.fontManager.SetFont(pdf, "", 9) // 使用等宽字体显示JSON(支持中文) + pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false) + } else { + // 如果没有JSON,尝试使用表格方式处理 + if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "response_example"); err != nil { + pb.logger.Warn("渲染响应示例表格失败,回退到文本显示", zap.Error(err)) + // 如果表格渲染失败,显示为文本 + text := pb.textProcessor.CleanText(doc.ResponseExample) + if strings.TrimSpace(text) != "" { + pb.fontManager.SetFont(pdf, "", 10) + pdf.SetTextColor(0, 0, 0) // 确保深黑色 + pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) + } + } + } + } + + // 返回字段说明 + if doc.ResponseFields != "" { + pdf.Ln(8) + // 显示标题 + pdf.SetTextColor(0, 0, 0) + pb.fontManager.SetFont(pdf, "B", 14) + _, lineHt = pdf.GetFontSize() + pdf.CellFormat(0, lineHt, "返回字段说明:", "", 1, "L", false, 0, "") + + // 使用新的数据库驱动方式处理返回字段(支持多个表格,带标题) + if err := pb.tableParser.ParseAndRenderTablesWithTitles(context.Background(), pdf, doc, "response_fields"); err != nil { + pb.logger.Warn("渲染返回字段表格失败,回退到文本显示", + zap.Error(err), + zap.String("content_preview", pb.getContentPreview(doc.ResponseFields, 200))) + // 如果表格渲染失败,显示为文本 + text := pb.textProcessor.CleanText(doc.ResponseFields) + if strings.TrimSpace(text) != "" { + pb.fontManager.SetFont(pdf, "", 10) + pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) + } else { + pb.logger.Warn("返回字段内容为空或只有空白字符") + } + } + } else { + pb.logger.Debug("返回字段内容为空,跳过渲染") + } + + // 错误代码 + if doc.ErrorCodes != "" { + pdf.Ln(8) + // 显示标题 + pdf.SetTextColor(0, 0, 0) + pb.fontManager.SetFont(pdf, "B", 14) + _, lineHt = pdf.GetFontSize() + pdf.CellFormat(0, lineHt, "错误代码:", "", 1, "L", false, 0, "") + + // 使用新的数据库驱动方式处理错误代码 + if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "error_codes"); err != nil { + pb.logger.Warn("渲染错误代码表格失败,回退到文本显示", zap.Error(err)) + // 如果表格渲染失败,显示为文本 + text := pb.textProcessor.CleanText(doc.ErrorCodes) + if strings.TrimSpace(text) != "" { + pb.fontManager.SetFont(pdf, "", 10) + pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) + } + } + } + // 注意:这里不添加二维码和说明,由调用方统一添加 +} + +// AddSubProductDocumentationPages 添加子产品的接口文档页面(用于组合包) +func (pb *PageBuilder) AddSubProductDocumentationPages(pdf *gofpdf.Fpdf, subProduct *entities.Product, doc *entities.ProductDocumentation, chineseFontAvailable bool, isLastSubProduct bool) { + // 创建自定义的AddPage函数,确保每页都有水印 + addPageWithWatermark := func() { + pdf.AddPage() + pb.addHeader(pdf, chineseFontAvailable) + pb.addWatermark(pdf, chineseFontAvailable) // 每页都添加水印 + } + + addPageWithWatermark() + + pdf.SetY(45) + pb.fontManager.SetFont(pdf, "B", 18) + _, lineHt := pdf.GetFontSize() + + // 显示子产品标题 + subProductTitle := fmt.Sprintf("子产品接口文档:%s", subProduct.Name) + if subProduct.Code != "" { + subProductTitle = fmt.Sprintf("子产品接口文档:%s (%s)", subProduct.Name, subProduct.Code) + } + pdf.CellFormat(0, lineHt, subProductTitle, "", 1, "L", false, 0, "") + + // 请求URL + pdf.Ln(8) + pdf.SetTextColor(0, 0, 0) // 确保深黑色 + pb.fontManager.SetFont(pdf, "B", 12) + pdf.CellFormat(0, lineHt, "请求URL:", "", 1, "L", false, 0, "") + // URL使用黑体字体(可能包含中文字符) + // 先清理URL中的乱码 + cleanURL := pb.textProcessor.CleanText(doc.RequestURL) + pb.fontManager.SetFont(pdf, "", 10) // 使用黑体 + pdf.SetTextColor(0, 0, 0) + pdf.MultiCell(0, lineHt*1.2, cleanURL, "", "L", false) + + // 请求方法 + pdf.Ln(5) + pb.fontManager.SetFont(pdf, "B", 12) + pdf.CellFormat(0, lineHt, fmt.Sprintf("请求方法:%s", doc.RequestMethod), "", 1, "L", false, 0, "") + + // 基本信息 + if doc.BasicInfo != "" { + pdf.Ln(8) + pb.addSection(pdf, "基本信息", doc.BasicInfo, chineseFontAvailable) + } + + // 请求参数 + if doc.RequestParams != "" { + pdf.Ln(8) + // 显示标题 + pdf.SetTextColor(0, 0, 0) + pb.fontManager.SetFont(pdf, "B", 14) + _, lineHt = pdf.GetFontSize() + pdf.CellFormat(0, lineHt, "请求参数:", "", 1, "L", false, 0, "") + + // 使用新的数据库驱动方式处理请求参数 + if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "request_params"); err != nil { + pb.logger.Warn("渲染请求参数表格失败,回退到文本显示", zap.Error(err)) + // 如果表格渲染失败,显示为文本 + text := pb.textProcessor.CleanText(doc.RequestParams) + if strings.TrimSpace(text) != "" { + pb.fontManager.SetFont(pdf, "", 10) + pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) + } + } + + // 生成JSON示例 + if jsonExample := pb.jsonProcessor.GenerateJSONExample(doc.RequestParams, pb.tableParser); jsonExample != "" { + pdf.Ln(5) + pdf.SetTextColor(0, 0, 0) // 确保深黑色 + pb.fontManager.SetFont(pdf, "B", 14) + pdf.CellFormat(0, lineHt, "请求示例:", "", 1, "L", false, 0, "") + // JSON中可能包含中文值,使用黑体字体 + pb.fontManager.SetFont(pdf, "", 9) // 使用黑体显示JSON(支持中文) + pdf.SetTextColor(0, 0, 0) + pdf.MultiCell(0, lineHt*1.3, jsonExample, "", "L", false) + } + } + + // 响应示例 + if doc.ResponseExample != "" { + pdf.Ln(8) + pdf.SetTextColor(0, 0, 0) // 确保深黑色 + pb.fontManager.SetFont(pdf, "B", 14) + _, lineHt = pdf.GetFontSize() + pdf.CellFormat(0, lineHt, "响应示例:", "", 1, "L", false, 0, "") + + // 优先尝试提取和格式化JSON + jsonContent := pb.jsonProcessor.ExtractJSON(doc.ResponseExample) + if jsonContent != "" { + // 格式化JSON + formattedJSON, err := pb.jsonProcessor.FormatJSON(jsonContent) + if err == nil { + jsonContent = formattedJSON + } + pdf.SetTextColor(0, 0, 0) // 确保深黑色 + pb.fontManager.SetFont(pdf, "", 9) // 使用等宽字体显示JSON(支持中文) + pdf.MultiCell(0, lineHt*1.3, jsonContent, "", "L", false) + } else { + // 如果没有JSON,尝试使用表格方式处理 + if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "response_example"); err != nil { + pb.logger.Warn("渲染响应示例表格失败,回退到文本显示", zap.Error(err)) + // 如果表格渲染失败,显示为文本 + text := pb.textProcessor.CleanText(doc.ResponseExample) + if strings.TrimSpace(text) != "" { + pb.fontManager.SetFont(pdf, "", 10) + pdf.SetTextColor(0, 0, 0) // 确保深黑色 + pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) + } + } + } + } + + // 返回字段说明 + if doc.ResponseFields != "" { + pdf.Ln(8) + // 显示标题 + pdf.SetTextColor(0, 0, 0) + pb.fontManager.SetFont(pdf, "B", 14) + _, lineHt = pdf.GetFontSize() + pdf.CellFormat(0, lineHt, "返回字段说明:", "", 1, "L", false, 0, "") + + // 使用新的数据库驱动方式处理返回字段(支持多个表格,带标题) + if err := pb.tableParser.ParseAndRenderTablesWithTitles(context.Background(), pdf, doc, "response_fields"); err != nil { + pb.logger.Warn("渲染返回字段表格失败,回退到文本显示", + zap.Error(err), + zap.String("content_preview", pb.getContentPreview(doc.ResponseFields, 200))) + // 如果表格渲染失败,显示为文本 + text := pb.textProcessor.CleanText(doc.ResponseFields) + if strings.TrimSpace(text) != "" { + pb.fontManager.SetFont(pdf, "", 10) + pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) + } else { + pb.logger.Warn("返回字段内容为空或只有空白字符") + } + } + } else { + pb.logger.Debug("返回字段内容为空,跳过渲染") + } + + // 错误代码 + if doc.ErrorCodes != "" { + pdf.Ln(8) + // 显示标题 + pdf.SetTextColor(0, 0, 0) + pb.fontManager.SetFont(pdf, "B", 14) + _, lineHt = pdf.GetFontSize() + pdf.CellFormat(0, lineHt, "错误代码:", "", 1, "L", false, 0, "") + + // 使用新的数据库驱动方式处理错误代码 + if err := pb.tableParser.ParseAndRenderTable(context.Background(), pdf, doc, "error_codes"); err != nil { + pb.logger.Warn("渲染错误代码表格失败,回退到文本显示", zap.Error(err)) + // 如果表格渲染失败,显示为文本 + text := pb.textProcessor.CleanText(doc.ErrorCodes) + if strings.TrimSpace(text) != "" { + pb.fontManager.SetFont(pdf, "", 10) + pdf.MultiCell(0, lineHt*1.3, text, "", "L", false) + } + } + } + // 注意:这里不添加二维码和说明,由调用方统一添加 +} + // addSection 添加章节 func (pb *PageBuilder) addSection(pdf *gofpdf.Fpdf, title, content string, chineseFontAvailable bool) { _, lineHt := pdf.GetFontSize() @@ -835,7 +1154,12 @@ func (pb *PageBuilder) safeSplitText(pdf *gofpdf.Fpdf, text string, width float6 return lines } -// addAdditionalInfo 添加说明文字和二维码 +// AddAdditionalInfo 添加说明文字和二维码(公开方法) +func (pb *PageBuilder) AddAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) { + pb.addAdditionalInfo(pdf, doc, chineseFontAvailable) +} + +// addAdditionalInfo 添加说明文字和二维码(私有方法) func (pb *PageBuilder) addAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.ProductDocumentation, chineseFontAvailable bool) { // 检查是否需要换页 pageWidth, pageHeight := pdf.GetPageSize() @@ -925,11 +1249,36 @@ func (pb *PageBuilder) addAdditionalInfo(pdf *gofpdf.Fpdf, doc *entities.Product // readExplanationText 读取说明文本文件 func (pb *PageBuilder) readExplanationText() string { resourcesPDFDir := GetResourcesPDFDir() + if resourcesPDFDir == "" { + pb.logger.Error("无法获取resources/pdf目录路径") + return "" + } + textFilePath := filepath.Join(resourcesPDFDir, "后勤服务.txt") + // 记录尝试读取的文件路径 + pb.logger.Debug("尝试读取说明文本文件", zap.String("path", textFilePath)) + // 检查文件是否存在 - if _, err := os.Stat(textFilePath); os.IsNotExist(err) { - pb.logger.Warn("说明文本文件不存在", zap.String("path", textFilePath)) + fileInfo, err := os.Stat(textFilePath) + if err != nil { + if os.IsNotExist(err) { + pb.logger.Warn("说明文本文件不存在", + zap.String("path", textFilePath), + zap.String("resources_dir", resourcesPDFDir), + ) + } else { + pb.logger.Error("检查说明文本文件时出错", + zap.String("path", textFilePath), + zap.Error(err), + ) + } + return "" + } + + // 检查文件大小 + if fileInfo.Size() == 0 { + pb.logger.Warn("说明文本文件为空", zap.String("path", textFilePath)) return "" } @@ -946,10 +1295,21 @@ func (pb *PageBuilder) readExplanationText() string { // 转换为字符串 text := string(content) + // 检查内容是否为空(去除空白字符后) + trimmedText := strings.TrimSpace(text) + if trimmedText == "" { + pb.logger.Warn("说明文本文件内容为空(只有空白字符)", + zap.String("path", textFilePath), + zap.Int("file_size", len(content)), + ) + return "" + } + // 记录读取成功的信息 pb.logger.Info("成功读取说明文本文件", zap.String("path", textFilePath), - zap.Int("file_size", len(content)), + zap.Int64("file_size", fileInfo.Size()), + zap.Int("content_length", len(content)), zap.Int("text_length", len(text)), zap.Int("line_count", len(strings.Split(text, "\n"))), ) diff --git a/internal/shared/pdf/pdf_generator_refactored.go b/internal/shared/pdf/pdf_generator_refactored.go index a2cae43..9fc6271 100644 --- a/internal/shared/pdf/pdf_generator_refactored.go +++ b/internal/shared/pdf/pdf_generator_refactored.go @@ -90,16 +90,21 @@ func (g *PDFGeneratorRefactored) GenerateProductPDF(ctx context.Context, product product.Price = decimal.NewFromFloat(price) } - return g.generatePDF(product, doc) + return g.generatePDF(product, doc, nil) } // GenerateProductPDFFromEntity 从entity类型生成PDF(推荐使用) func (g *PDFGeneratorRefactored) GenerateProductPDFFromEntity(ctx context.Context, product *entities.Product, doc *entities.ProductDocumentation) ([]byte, error) { - return g.generatePDF(product, doc) + return g.generatePDF(product, doc, nil) +} + +// GenerateProductPDFWithSubProducts 从entity类型生成PDF,支持组合包子产品文档 +func (g *PDFGeneratorRefactored) GenerateProductPDFWithSubProducts(ctx context.Context, product *entities.Product, doc *entities.ProductDocumentation, subProductDocs []*entities.ProductDocumentation) ([]byte, error) { + return g.generatePDF(product, doc, subProductDocs) } // generatePDF 内部PDF生成方法 -func (g *PDFGeneratorRefactored) generatePDF(product *entities.Product, doc *entities.ProductDocumentation) (result []byte, err error) { +func (g *PDFGeneratorRefactored) generatePDF(product *entities.Product, doc *entities.ProductDocumentation, subProductDocs []*entities.ProductDocumentation) (result []byte, err error) { defer func() { if r := recover(); r != nil { // 将panic转换为error,而不是重新抛出 @@ -174,9 +179,61 @@ func (g *PDFGeneratorRefactored) generatePDF(product *entities.Product, doc *ent // 添加第一页(产品信息) pageBuilder.AddFirstPage(pdf, product, doc, chineseFontAvailable) - // 如果有关联的文档,添加接口文档页面 - if doc != nil { - pageBuilder.AddDocumentationPages(pdf, doc, chineseFontAvailable) + // 如果是组合包,需要特殊处理:先渲染所有文档,最后统一添加二维码 + if product.IsPackage { + // 如果有关联的文档,添加接口文档页面(但不包含二维码和说明,后面统一添加) + if doc != nil { + pageBuilder.AddDocumentationPagesWithoutAdditionalInfo(pdf, doc, chineseFontAvailable) + } + + // 如果有子产品文档,为每个子产品添加接口文档页面 + if len(subProductDocs) > 0 { + for i, subDoc := range subProductDocs { + // 获取子产品信息(从文档中获取ProductID,然后查找对应的产品信息) + // 注意:这里我们需要从product.PackageItems中查找对应的子产品信息 + var subProduct *entities.Product + if product.PackageItems != nil && i < len(product.PackageItems) { + if product.PackageItems[i].Product != nil { + subProduct = product.PackageItems[i].Product + } + } + + // 如果找不到子产品信息,创建一个基本的子产品实体 + if subProduct == nil { + subProduct = &entities.Product{ + ID: subDoc.ProductID, + Code: subDoc.ProductID, // 使用ProductID作为临时Code + Name: fmt.Sprintf("子产品 %d", i+1), + } + } + + pageBuilder.AddSubProductDocumentationPages(pdf, subProduct, subDoc, chineseFontAvailable, false) + } + } + + // 在所有接口文档渲染完成后,统一添加二维码和后勤服务说明 + // 使用主产品文档(如果存在),否则使用第一个子产品文档,如果都没有则创建一个空的文档对象 + var finalDoc *entities.ProductDocumentation + if doc != nil { + finalDoc = doc + } else if len(subProductDocs) > 0 { + finalDoc = subProductDocs[0] + } else { + // 如果没有文档,创建一个空的文档对象,用于添加二维码和说明 + finalDoc = &entities.ProductDocumentation{ + ProductID: product.ID, + RequestMethod: "POST", + Version: "1.0", + } + } + + // 始终添加二维码和后勤服务说明 + pageBuilder.AddAdditionalInfo(pdf, finalDoc, chineseFontAvailable) + } else { + // 普通产品:使用原来的方法(包含二维码和说明) + if doc != nil { + pageBuilder.AddDocumentationPages(pdf, doc, chineseFontAvailable) + } } // 生成PDF字节流