Files
2025-12-22 18:32:34 +08:00

505 lines
14 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package component_report
import (
"archive/zip"
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"go.uber.org/zap"
)
// ZipGenerator ZIP文件生成器
type ZipGenerator struct {
logger *zap.Logger
// 缓存配置
CacheEnabled bool
CacheDir string
CacheTTL time.Duration
}
// NewZipGenerator 创建ZIP文件生成器
func NewZipGenerator(logger *zap.Logger) *ZipGenerator {
return &ZipGenerator{
logger: logger,
CacheEnabled: true,
CacheDir: "storage/component-reports/cache",
CacheTTL: 24 * time.Hour, // 默认缓存24小时
}
}
// NewZipGeneratorWithCache 创建带有自定义缓存配置的ZIP文件生成器
func NewZipGeneratorWithCache(logger *zap.Logger, cacheEnabled bool, cacheDir string, cacheTTL time.Duration) *ZipGenerator {
return &ZipGenerator{
logger: logger,
CacheEnabled: cacheEnabled,
CacheDir: cacheDir,
CacheTTL: cacheTTL,
}
}
// GenerateZipFile 生成ZIP文件包含 example.json 和根据子产品编码匹配的UI组件文件
// productID: 产品ID
// subProductCodes: 子产品编码列表用于过滤和下载匹配的UI组件
// exampleJSONGenerator: 示例JSON生成器
// outputPath: 输出ZIP文件路径如果为空则使用默认路径
func (g *ZipGenerator) GenerateZipFile(
ctx context.Context,
productID string,
subProductCodes []string,
exampleJSONGenerator *ExampleJSONGenerator,
outputPath string,
) (string, error) {
// 生成缓存键
cacheKey := g.generateCacheKey(productID, subProductCodes)
// 检查缓存
if g.CacheEnabled {
cachedPath, err := g.getCachedFile(cacheKey)
if err == nil && cachedPath != "" {
// g.logger.Debug("使用缓存的ZIP文件",
// zap.String("product_id", productID),
// zap.String("cache_path", cachedPath))
// 如果指定了输出路径,复制缓存文件到目标位置
if outputPath != "" && outputPath != cachedPath {
if err := g.copyFile(cachedPath, outputPath); err != nil {
g.logger.Error("复制缓存文件失败", zap.Error(err))
} else {
return outputPath, nil
}
}
return cachedPath, nil
}
}
// 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. 将生成的内容添加到 Pure_Component/public 目录下的 example.json
exampleWriter, err := zipWriter.Create("Pure_Component/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. 添加整个 Pure_Component 目录但只包含子产品编码匹配的UI组件文件
srcBasePath := filepath.Join("resources", "Pure_Component")
uiBasePath := filepath.Join(srcBasePath, "src", "ui")
// 根据子产品编码收集所有匹配的组件名称(文件夹名或文件名)
matchedNames := make(map[string]bool)
for _, subProductCode := range subProductCodes {
path, _, err := exampleJSONGenerator.MatchSubProductCodeToPath(ctx, subProductCode)
if err == nil && path != "" {
// 获取组件名称(文件夹名或文件名)
componentName := filepath.Base(path)
matchedNames[componentName] = true
}
}
// 遍历整个 Pure_Component 目录
err = filepath.Walk(srcBasePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 计算相对于 Pure_Component 的路径
relPath, err := filepath.Rel(srcBasePath, path)
if err != nil {
return err
}
// 转换为ZIP路径格式保持在Pure_Component目录下
zipPath := filepath.ToSlash(filepath.Join("Pure_Component", 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()
// 检查是否应该保留:匹配到的组件文件夹/文件
shouldInclude := false
// 检查是否是匹配的组件(检查组件名称)
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("添加Pure_Component目录失败", 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)),
)
// 缓存文件
if g.CacheEnabled {
if err := g.cacheFile(outputPath, cacheKey); err != nil {
g.logger.Warn("缓存ZIP文件失败", zap.Error(err))
} else {
g.logger.Debug("ZIP文件已缓存", zap.String("cache_key", cacheKey))
}
}
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)
})
}
// GenerateFilteredComponentZip 生成筛选后的组件ZIP文件
// productID: 产品ID
// subProductCodes: 子产品编号列表(用于筛选组件)
// outputPath: 输出ZIP文件路径如果为空则使用默认路径
func (g *ZipGenerator) GenerateFilteredComponentZip(
ctx context.Context,
productID string,
subProductCodes []string,
outputPath string,
) (string, error) {
// 1. 确定基础路径
basePath := filepath.Join("resources", "Pure_Component")
uiBasePath := filepath.Join(basePath, "src", "ui")
// 2. 确定输出路径
if outputPath == "" {
// 使用默认路径storage/component-reports/{productID}_filtered.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_filtered.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. 收集所有匹配的组件名称(文件夹名或文件名)
matchedNames := make(map[string]bool)
for _, productCode := range subProductCodes {
// 简化匹配逻辑,直接使用产品代码作为组件名
matchedNames[productCode] = true
}
// 5. 递归添加整个 Pure_Component 目录,但筛选 ui 目录下的内容
err = filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 计算相对于基础路径的相对路径
relPath, err := filepath.Rel(basePath, path)
if err != nil {
return err
}
// 转换为ZIP路径格式保持在Pure_Component目录下
zipPath := filepath.ToSlash(filepath.Join("Pure_Component", 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()
// 检查是否应该保留:匹配到的组件文件夹/文件
shouldInclude := false
// 检查是否是匹配的组件(检查组件名称)
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("添加Pure_Component目录失败", zap.Error(err))
return "", fmt.Errorf("添加Pure_Component目录失败: %w", err)
}
g.logger.Info("成功生成筛选后的组件ZIP文件",
zap.String("product_id", productID),
zap.String("output_path", outputPath),
zap.Int("matched_components_count", len(matchedNames)),
)
return outputPath, nil
}
// generateCacheKey 生成缓存键
func (g *ZipGenerator) generateCacheKey(productID string, subProductCodes []string) string {
// 使用产品ID和子产品编码列表生成MD5哈希
data := productID
for _, code := range subProductCodes {
data += "|" + code
}
hash := md5.Sum([]byte(data))
return hex.EncodeToString(hash[:])
}
// getCachedFile 获取缓存文件
func (g *ZipGenerator) getCachedFile(cacheKey string) (string, error) {
// 确保缓存目录存在
if err := os.MkdirAll(g.CacheDir, 0755); err != nil {
return "", fmt.Errorf("创建缓存目录失败: %w", err)
}
cacheFilePath := filepath.Join(g.CacheDir, cacheKey+".zip")
// 检查文件是否存在
fileInfo, err := os.Stat(cacheFilePath)
if os.IsNotExist(err) {
return "", nil // 文件不存在,但不是错误
}
if err != nil {
return "", err
}
// 检查文件是否过期
if time.Since(fileInfo.ModTime()) > g.CacheTTL {
// 文件过期,删除
os.Remove(cacheFilePath)
return "", nil
}
return cacheFilePath, nil
}
// cacheFile 缓存文件
func (g *ZipGenerator) cacheFile(filePath, cacheKey string) error {
// 确保缓存目录存在
if err := os.MkdirAll(g.CacheDir, 0755); err != nil {
return fmt.Errorf("创建缓存目录失败: %w", err)
}
cacheFilePath := filepath.Join(g.CacheDir, cacheKey+".zip")
// 复制文件到缓存目录
return g.copyFile(filePath, cacheFilePath)
}
// copyFile 复制文件
func (g *ZipGenerator) copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
return err
}