package component_report import ( "archive/zip" "context" "fmt" "io" "os" "path/filepath" "strings" "go.uber.org/zap" ) // ZipGenerator ZIP文件生成器 type ZipGenerator struct { logger *zap.Logger } // NewZipGenerator 创建ZIP文件生成器 func NewZipGenerator(logger *zap.Logger) *ZipGenerator { return &ZipGenerator{ logger: logger, } } // GenerateZipFile 生成ZIP文件,包含 example.json 和匹配的组件文件 // productID: 产品ID // subProductCodes: 子产品编号列表(如果为空,则处理所有子产品) // exampleJSONGenerator: 示例JSON生成器 // outputPath: 输出ZIP文件路径(如果为空,则使用默认路径) func (g *ZipGenerator) GenerateZipFile( ctx context.Context, productID string, subProductCodes []string, exampleJSONGenerator *ExampleJSONGenerator, outputPath string, ) (string, error) { // 1. 生成 example.json 内容 exampleJSON, err := exampleJSONGenerator.GenerateExampleJSON(ctx, productID, subProductCodes) if err != nil { return "", fmt.Errorf("生成example.json失败: %w", err) } // 2. 确定输出路径 if outputPath == "" { // 使用默认路径:storage/component-reports/{productID}.zip outputDir := "storage/component-reports" if err := os.MkdirAll(outputDir, 0755); err != nil { return "", fmt.Errorf("创建输出目录失败: %w", err) } outputPath = filepath.Join(outputDir, fmt.Sprintf("%s_example.json.zip", productID)) } // 3. 创建ZIP文件 zipFile, err := os.Create(outputPath) if err != nil { return "", fmt.Errorf("创建ZIP文件失败: %w", err) } defer zipFile.Close() zipWriter := zip.NewWriter(zipFile) defer zipWriter.Close() // 4. 添加 example.json 到 public 目录 exampleWriter, err := zipWriter.Create("public/example.json") if err != nil { return "", fmt.Errorf("创建example.json文件失败: %w", err) } _, err = exampleWriter.Write(exampleJSON) if err != nil { return "", fmt.Errorf("写入example.json失败: %w", err) } // 5. 添加整个 src 目录,但过滤 ui 目录下的文件 srcBasePath := filepath.Join("resources", "Pure Component", "src") uiBasePath := filepath.Join(srcBasePath, "ui") // 收集所有匹配的组件名称(文件夹名或文件名) matchedNames := make(map[string]bool) for _, productCode := range subProductCodes { path, _, err := exampleJSONGenerator.MatchProductCodeToPath(ctx, productCode) if err == nil && path != "" { // 获取组件名称(文件夹名或文件名) componentName := filepath.Base(path) matchedNames[componentName] = true } } // 遍历整个 src 目录 err = filepath.Walk(srcBasePath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // 计算相对于 src 的路径 relPath, err := filepath.Rel(srcBasePath, path) if err != nil { return err } // 转换为ZIP路径格式 zipPath := filepath.ToSlash(filepath.Join("src", relPath)) // 检查是否在 ui 目录下 uiRelPath, err := filepath.Rel(uiBasePath, path) isInUIDir := err == nil && !strings.HasPrefix(uiRelPath, "..") if isInUIDir { // 如果是 ui 目录本身,直接添加 if uiRelPath == "." || uiRelPath == "" { if info.IsDir() { _, err = zipWriter.Create(zipPath + "/") return err } return nil } // 获取文件/文件夹名称 fileName := info.Name() // 检查是否应该保留: // 1. CBehaviorRiskScan.vue 文件(无论在哪里) // 2. 匹配到的组件文件夹/文件 shouldInclude := false // 检查是否是 CBehaviorRiskScan.vue if fileName == "CBehaviorRiskScan.vue" { shouldInclude = true } else { // 检查是否是匹配的组件(检查组件名称) if matchedNames[fileName] { shouldInclude = true } else { // 检查是否在匹配的组件文件夹内 // 获取相对于 ui 的路径的第一部分(组件文件夹名) parts := strings.Split(filepath.ToSlash(uiRelPath), "/") if len(parts) > 0 && parts[0] != "" && parts[0] != "." { if matchedNames[parts[0]] { shouldInclude = true } } } } if !shouldInclude { // 跳过不匹配的文件/文件夹 if info.IsDir() { return filepath.SkipDir } return nil } } // 如果是目录,创建目录项 if info.IsDir() { _, err = zipWriter.Create(zipPath + "/") return err } // 添加文件 return g.AddFileToZip(zipWriter, path, zipPath) }) if err != nil { g.logger.Warn("添加src目录失败", zap.Error(err)) } g.logger.Info("成功生成ZIP文件", zap.String("product_id", productID), zap.String("output_path", outputPath), zap.Int("example_json_size", len(exampleJSON)), zap.Int("sub_product_count", len(subProductCodes)), ) return outputPath, nil } // AddFileToZip 添加文件到ZIP func (g *ZipGenerator) AddFileToZip(zipWriter *zip.Writer, filePath string, zipPath string) error { file, err := os.Open(filePath) if err != nil { return fmt.Errorf("打开文件失败: %w", err) } defer file.Close() writer, err := zipWriter.Create(zipPath) if err != nil { return fmt.Errorf("创建ZIP文件项失败: %w", err) } _, err = io.Copy(writer, file) if err != nil { return fmt.Errorf("复制文件内容失败: %w", err) } return nil } // AddFolderToZip 递归添加文件夹到ZIP func (g *ZipGenerator) AddFolderToZip(zipWriter *zip.Writer, folderPath string, basePath string) error { return filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } // 计算相对路径 relPath, err := filepath.Rel(basePath, path) if err != nil { return err } // 转换为ZIP路径格式(使用正斜杠) zipPath := filepath.ToSlash(relPath) return g.AddFileToZip(zipWriter, path, zipPath) }) } // AddFileToZipWithTarget 将单个文件添加到ZIP的指定目标路径 func (g *ZipGenerator) AddFileToZipWithTarget(zipWriter *zip.Writer, filePath string, targetPath string) error { file, err := os.Open(filePath) if err != nil { return fmt.Errorf("打开文件失败: %w", err) } defer file.Close() writer, err := zipWriter.Create(filepath.ToSlash(targetPath)) if err != nil { return fmt.Errorf("创建ZIP文件项失败: %w", err) } _, err = io.Copy(writer, file) if err != nil { return fmt.Errorf("复制文件内容失败: %w", err) } return nil } // AddFolderToZipWithPrefix 递归添加文件夹到ZIP,并在ZIP内添加路径前缀 func (g *ZipGenerator) AddFolderToZipWithPrefix(zipWriter *zip.Writer, folderPath string, basePath string, prefix string) error { return filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } relPath, err := filepath.Rel(basePath, path) if err != nil { return err } zipPath := filepath.ToSlash(filepath.Join(prefix, relPath)) return g.AddFileToZip(zipWriter, path, zipPath) }) }