f
This commit is contained in:
211
internal/shared/component_report/README.md
Normal file
211
internal/shared/component_report/README.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# 组件报告生成服务
|
||||
|
||||
这个服务用于生成产品示例报告的 `example.json` 文件,并打包成 ZIP 文件供下载。
|
||||
|
||||
## 功能概述
|
||||
|
||||
1. **生成 example.json 文件**:根据组合包子产品的响应示例数据生成符合格式要求的 JSON 文件
|
||||
2. **打包 ZIP 文件**:将生成的 `example.json` 文件打包成 ZIP 格式
|
||||
3. **HTTP 接口**:提供 HTTP 接口用于生成和下载文件
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
component_report/
|
||||
├── example_json_generator.go # 示例JSON生成器
|
||||
├── zip_generator.go # ZIP文件生成器
|
||||
├── handler.go # HTTP处理器
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 直接使用生成器
|
||||
|
||||
```go
|
||||
// 创建生成器
|
||||
exampleJSONGenerator := component_report.NewExampleJSONGenerator(
|
||||
productRepo,
|
||||
docRepo,
|
||||
apiConfigRepo,
|
||||
logger,
|
||||
)
|
||||
|
||||
// 生成 example.json
|
||||
jsonData, err := exampleJSONGenerator.GenerateExampleJSON(
|
||||
ctx,
|
||||
productID, // 产品ID(可以是组合包或单品)
|
||||
subProductCodes, // 子产品编号列表(可选,如果为空则处理所有子产品)
|
||||
)
|
||||
```
|
||||
|
||||
### 2. 生成 ZIP 文件
|
||||
|
||||
```go
|
||||
// 创建ZIP生成器
|
||||
zipGenerator := component_report.NewZipGenerator(logger)
|
||||
|
||||
// 生成ZIP文件
|
||||
zipPath, err := zipGenerator.GenerateZipFile(
|
||||
ctx,
|
||||
productID,
|
||||
subProductCodes,
|
||||
exampleJSONGenerator,
|
||||
outputPath, // 输出路径(可选,如果为空则使用默认路径)
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 使用 HTTP 接口
|
||||
|
||||
#### 生成 example.json
|
||||
|
||||
```http
|
||||
POST /api/v1/component-report/generate-example-json
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"product_id": "产品ID",
|
||||
"sub_product_codes": ["子产品编号1", "子产品编号2"] // 可选
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"product_id": "产品ID",
|
||||
"json_content": "生成的JSON内容",
|
||||
"json_size": 1234
|
||||
}
|
||||
```
|
||||
|
||||
#### 生成 ZIP 文件
|
||||
|
||||
```http
|
||||
POST /api/v1/component-report/generate-zip
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"product_id": "产品ID",
|
||||
"sub_product_codes": ["子产品编号1", "子产品编号2"], // 可选
|
||||
"output_path": "自定义输出路径" // 可选
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "ZIP文件生成成功",
|
||||
"zip_path": "storage/component-reports/xxx_example.json.zip",
|
||||
"file_size": 12345,
|
||||
"file_name": "xxx_example.json.zip"
|
||||
}
|
||||
```
|
||||
|
||||
#### 生成并下载 ZIP 文件
|
||||
|
||||
```http
|
||||
POST /api/v1/component-report/generate-and-download
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"product_id": "产品ID",
|
||||
"sub_product_codes": ["子产品编号1", "子产品编号2"] // 可选
|
||||
}
|
||||
```
|
||||
|
||||
响应:直接返回 ZIP 文件流
|
||||
|
||||
#### 下载已生成的 ZIP 文件
|
||||
|
||||
```http
|
||||
GET /api/v1/component-report/download-zip/:product_id
|
||||
```
|
||||
|
||||
响应:直接返回 ZIP 文件流
|
||||
|
||||
## example.json 格式
|
||||
|
||||
生成的 `example.json` 文件格式如下:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"feature": {
|
||||
"featureName": "产品名称",
|
||||
"sort": 1
|
||||
},
|
||||
"data": {
|
||||
"apiID": "产品编号",
|
||||
"data": {
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": { ... }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"feature": {
|
||||
"featureName": "另一个产品名称",
|
||||
"sort": 2
|
||||
},
|
||||
"data": {
|
||||
"apiID": "另一个产品编号",
|
||||
"data": { ... }
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 响应示例数据提取优先级
|
||||
|
||||
1. **产品文档的 `response_example` 字段**(JSON格式)
|
||||
2. **产品文档的 `response_example` 字段**(Markdown代码块中的JSON)
|
||||
3. **产品API配置的 `response_example` 字段**
|
||||
4. **默认空对象** `{}`(如果都没有)
|
||||
|
||||
## ZIP 文件结构
|
||||
|
||||
生成的 ZIP 文件结构:
|
||||
|
||||
```
|
||||
component-report.zip
|
||||
└── public/
|
||||
└── example.json
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保 `storage/component-reports` 目录存在且有写权限
|
||||
2. 如果产品是组合包,会遍历所有子产品(或指定的子产品)生成响应示例
|
||||
3. 如果某个子产品没有响应示例数据,会使用空对象 `{}` 作为默认值
|
||||
4. ZIP 文件会保存在 `storage/component-reports` 目录下,文件名为 `{productID}_example.json.zip`
|
||||
|
||||
## 集成到路由
|
||||
|
||||
如果需要使用 HTTP 接口,需要在路由中注册:
|
||||
|
||||
```go
|
||||
// 创建处理器
|
||||
componentReportHandler := component_report.NewComponentReportHandler(
|
||||
productRepo,
|
||||
docRepo,
|
||||
apiConfigRepo,
|
||||
componentReportRepo,
|
||||
purchaseOrderRepo,
|
||||
rechargeRecordRepo,
|
||||
alipayOrderRepo,
|
||||
wechatOrderRepo,
|
||||
aliPayService,
|
||||
wechatPayService,
|
||||
logger,
|
||||
)
|
||||
|
||||
// 注册路由
|
||||
router.POST("/api/v1/component-report/generate-example-json", componentReportHandler.GenerateExampleJSON)
|
||||
router.POST("/api/v1/component-report/generate-zip", componentReportHandler.GenerateZip)
|
||||
router.POST("/api/v1/component-report/generate-and-download", componentReportHandler.GenerateAndDownloadZip)
|
||||
router.GET("/api/v1/component-report/download-zip/:product_id", componentReportHandler.DownloadZip)
|
||||
```
|
||||
137
internal/shared/component_report/cache_manager.go
Normal file
137
internal/shared/component_report/cache_manager.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package component_report
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CacheManager 缓存管理器
|
||||
type CacheManager struct {
|
||||
cacheDir string
|
||||
ttl time.Duration
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCacheManager 创建缓存管理器
|
||||
func NewCacheManager(cacheDir string, ttl time.Duration, logger *zap.Logger) *CacheManager {
|
||||
return &CacheManager{
|
||||
cacheDir: cacheDir,
|
||||
ttl: ttl,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CleanExpiredCache 清理过期缓存
|
||||
func (cm *CacheManager) CleanExpiredCache() error {
|
||||
// 确保缓存目录存在
|
||||
if _, err := os.Stat(cm.cacheDir); os.IsNotExist(err) {
|
||||
return nil // 目录不存在,无需清理
|
||||
}
|
||||
|
||||
// 遍历缓存目录
|
||||
err := filepath.Walk(cm.cacheDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 跳过目录
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查文件是否过期
|
||||
if time.Since(info.ModTime()) > cm.ttl {
|
||||
// cm.logger.Debug("删除过期缓存文件",
|
||||
// zap.String("path", path),
|
||||
// zap.Time("mod_time", info.ModTime()),
|
||||
// zap.Duration("age", time.Since(info.ModTime())))
|
||||
|
||||
if err := os.Remove(path); err != nil {
|
||||
cm.logger.Error("删除过期缓存文件失败",
|
||||
zap.Error(err),
|
||||
zap.String("path", path))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理过期缓存失败: %w", err)
|
||||
}
|
||||
|
||||
// cm.logger.Info("缓存清理完成", zap.String("cache_dir", cm.cacheDir))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCacheSize 获取缓存总大小
|
||||
func (cm *CacheManager) GetCacheSize() (int64, error) {
|
||||
var totalSize int64
|
||||
|
||||
err := filepath.Walk(cm.cacheDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
totalSize += info.Size()
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("计算缓存大小失败: %w", err)
|
||||
}
|
||||
|
||||
return totalSize, nil
|
||||
}
|
||||
|
||||
// GetCacheCount 获取缓存文件数量
|
||||
func (cm *CacheManager) GetCacheCount() (int, error) {
|
||||
var count int
|
||||
|
||||
err := filepath.Walk(cm.cacheDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
count++
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("统计缓存文件数量失败: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// ClearAllCache 清理所有缓存
|
||||
func (cm *CacheManager) ClearAllCache() error {
|
||||
// 确保缓存目录存在
|
||||
if _, err := os.Stat(cm.cacheDir); os.IsNotExist(err) {
|
||||
return nil // 目录不存在,无需清理
|
||||
}
|
||||
|
||||
err := os.RemoveAll(cm.cacheDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理所有缓存失败: %w", err)
|
||||
}
|
||||
|
||||
// 重新创建目录
|
||||
if err := os.MkdirAll(cm.cacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("重新创建缓存目录失败: %w", err)
|
||||
}
|
||||
|
||||
// cm.logger.Info("所有缓存已清理", zap.String("cache_dir", cm.cacheDir))
|
||||
return nil
|
||||
}
|
||||
422
internal/shared/component_report/example_json_generator.go
Normal file
422
internal/shared/component_report/example_json_generator.go
Normal file
@@ -0,0 +1,422 @@
|
||||
package component_report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"hyapi-server/internal/domains/product/entities"
|
||||
"hyapi-server/internal/domains/product/repositories"
|
||||
)
|
||||
|
||||
// ExampleJSONGenerator 示例JSON生成器
|
||||
type ExampleJSONGenerator struct {
|
||||
productRepo repositories.ProductRepository
|
||||
docRepo repositories.ProductDocumentationRepository
|
||||
apiConfigRepo repositories.ProductApiConfigRepository
|
||||
logger *zap.Logger
|
||||
// 缓存配置
|
||||
CacheEnabled bool
|
||||
CacheDir string
|
||||
CacheTTL time.Duration
|
||||
}
|
||||
|
||||
// NewExampleJSONGenerator 创建示例JSON生成器
|
||||
func NewExampleJSONGenerator(
|
||||
productRepo repositories.ProductRepository,
|
||||
docRepo repositories.ProductDocumentationRepository,
|
||||
apiConfigRepo repositories.ProductApiConfigRepository,
|
||||
logger *zap.Logger,
|
||||
) *ExampleJSONGenerator {
|
||||
return &ExampleJSONGenerator{
|
||||
productRepo: productRepo,
|
||||
docRepo: docRepo,
|
||||
apiConfigRepo: apiConfigRepo,
|
||||
logger: logger,
|
||||
CacheEnabled: true,
|
||||
CacheDir: "storage/component-reports/cache",
|
||||
CacheTTL: 24 * time.Hour, // 默认缓存24小时
|
||||
}
|
||||
}
|
||||
|
||||
// NewExampleJSONGeneratorWithCache 创建带有自定义缓存配置的示例JSON生成器
|
||||
func NewExampleJSONGeneratorWithCache(
|
||||
productRepo repositories.ProductRepository,
|
||||
docRepo repositories.ProductDocumentationRepository,
|
||||
apiConfigRepo repositories.ProductApiConfigRepository,
|
||||
logger *zap.Logger,
|
||||
cacheEnabled bool,
|
||||
cacheDir string,
|
||||
cacheTTL time.Duration,
|
||||
) *ExampleJSONGenerator {
|
||||
return &ExampleJSONGenerator{
|
||||
productRepo: productRepo,
|
||||
docRepo: docRepo,
|
||||
apiConfigRepo: apiConfigRepo,
|
||||
logger: logger,
|
||||
CacheEnabled: cacheEnabled,
|
||||
CacheDir: cacheDir,
|
||||
CacheTTL: cacheTTL,
|
||||
}
|
||||
}
|
||||
|
||||
// ExampleJSONItem example.json 中的单个项
|
||||
type ExampleJSONItem struct {
|
||||
Feature struct {
|
||||
FeatureName string `json:"featureName"`
|
||||
Sort int `json:"sort"`
|
||||
} `json:"feature"`
|
||||
Data struct {
|
||||
APIID string `json:"apiID"`
|
||||
Data interface{} `json:"data"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// GenerateExampleJSON 生成 example.json 文件内容
|
||||
// productID: 产品ID(可以是组合包或单品)
|
||||
// subProductCodes: 子产品编号列表(如果为空,则处理所有子产品)
|
||||
func (g *ExampleJSONGenerator) GenerateExampleJSON(ctx context.Context, productID string, subProductCodes []string) ([]byte, error) {
|
||||
// 生成缓存键
|
||||
cacheKey := g.generateCacheKey(productID, subProductCodes)
|
||||
|
||||
// 检查缓存
|
||||
if g.CacheEnabled {
|
||||
cachedData, err := g.getCachedData(cacheKey)
|
||||
if err == nil && cachedData != nil {
|
||||
// g.logger.Debug("使用缓存的example.json数据",
|
||||
// zap.String("product_id", productID),
|
||||
// zap.String("cache_key", cacheKey))
|
||||
return cachedData, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 获取产品信息
|
||||
product, err := g.productRepo.GetByID(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取产品信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 构建 example.json 数组
|
||||
var examples []ExampleJSONItem
|
||||
|
||||
if product.IsPackage {
|
||||
// 组合包:遍历子产品
|
||||
packageItems, err := g.productRepo.GetPackageItems(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取组合包子产品失败: %w", err)
|
||||
}
|
||||
|
||||
for sort, item := range packageItems {
|
||||
// 如果指定了子产品编号列表,只处理列表中的产品
|
||||
if len(subProductCodes) > 0 {
|
||||
found := false
|
||||
for _, code := range subProductCodes {
|
||||
if item.Product != nil && item.Product.Code == code {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 获取子产品信息
|
||||
var subProduct entities.Product
|
||||
if item.Product != nil {
|
||||
subProduct = *item.Product
|
||||
} else {
|
||||
subProduct, err = g.productRepo.GetByID(ctx, item.ProductID)
|
||||
if err != nil {
|
||||
g.logger.Warn("获取子产品信息失败",
|
||||
zap.String("product_id", item.ProductID),
|
||||
zap.Error(err),
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 获取响应示例数据
|
||||
responseData := g.extractResponseExample(ctx, &subProduct)
|
||||
|
||||
// 获取产品名称和编号
|
||||
productName := subProduct.Name
|
||||
productCode := subProduct.Code
|
||||
|
||||
// 构建示例项
|
||||
example := ExampleJSONItem{
|
||||
Feature: struct {
|
||||
FeatureName string `json:"featureName"`
|
||||
Sort int `json:"sort"`
|
||||
}{
|
||||
FeatureName: productName,
|
||||
Sort: sort + 1,
|
||||
},
|
||||
Data: struct {
|
||||
APIID string `json:"apiID"`
|
||||
Data interface{} `json:"data"`
|
||||
}{
|
||||
APIID: productCode,
|
||||
Data: responseData,
|
||||
},
|
||||
}
|
||||
|
||||
examples = append(examples, example)
|
||||
}
|
||||
} else {
|
||||
// 单品
|
||||
responseData := g.extractResponseExample(ctx, &product)
|
||||
|
||||
example := ExampleJSONItem{
|
||||
Feature: struct {
|
||||
FeatureName string `json:"featureName"`
|
||||
Sort int `json:"sort"`
|
||||
}{
|
||||
FeatureName: product.Name,
|
||||
Sort: 1,
|
||||
},
|
||||
Data: struct {
|
||||
APIID string `json:"apiID"`
|
||||
Data interface{} `json:"data"`
|
||||
}{
|
||||
APIID: product.Code,
|
||||
Data: responseData,
|
||||
},
|
||||
}
|
||||
|
||||
examples = append(examples, example)
|
||||
}
|
||||
|
||||
// 3. 序列化为JSON
|
||||
jsonData, err := json.MarshalIndent(examples, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("序列化example.json失败: %w", err)
|
||||
}
|
||||
|
||||
// 缓存数据
|
||||
if g.CacheEnabled {
|
||||
if err := g.cacheData(cacheKey, jsonData); err != nil {
|
||||
g.logger.Warn("缓存example.json数据失败", zap.Error(err))
|
||||
} else {
|
||||
g.logger.Debug("example.json数据已缓存", zap.String("cache_key", cacheKey))
|
||||
}
|
||||
}
|
||||
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
// MatchSubProductCodeToPath 根据子产品编码匹配 UI 组件路径,返回路径和类型(folder/file)
|
||||
func (g *ExampleJSONGenerator) MatchSubProductCodeToPath(ctx context.Context, subProductCode string) (string, string, error) {
|
||||
basePath := filepath.Join("resources", "Pure_Component", "src", "ui")
|
||||
|
||||
entries, err := os.ReadDir(basePath)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("读取组件目录失败: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
|
||||
// 使用改进的相似性匹配算法
|
||||
if isSimilarCode(subProductCode, name) {
|
||||
path := filepath.Join(basePath, name)
|
||||
fileType := "folder"
|
||||
if !entry.IsDir() {
|
||||
fileType = "file"
|
||||
}
|
||||
return path, fileType, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("未找到匹配的组件文件: %s", subProductCode)
|
||||
}
|
||||
|
||||
// extractCoreCode 提取文件名中的核心编码部分
|
||||
func extractCoreCode(name string) string {
|
||||
for i, r := range name {
|
||||
if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||
return name[i:]
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// extractMainCode 从子产品编码或文件夹名称中提取主要编码部分
|
||||
// 处理可能的格式差异,如前缀、后缀等
|
||||
func extractMainCode(code string) string {
|
||||
// 移除常见的前缀,如 C
|
||||
if len(code) > 0 && code[0] == 'C' {
|
||||
return code[1:]
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// isSimilarCode 判断两个编码是否相似,考虑多种可能的格式差异
|
||||
func isSimilarCode(code1, code2 string) bool {
|
||||
// 直接相等
|
||||
if code1 == code2 {
|
||||
return true
|
||||
}
|
||||
|
||||
// 移除常见前缀后比较
|
||||
mainCode1 := extractMainCode(code1)
|
||||
mainCode2 := extractMainCode(code2)
|
||||
if mainCode1 == mainCode2 || mainCode1 == code2 || code1 == mainCode2 {
|
||||
return true
|
||||
}
|
||||
|
||||
// 包含关系
|
||||
if strings.Contains(code1, code2) || strings.Contains(code2, code1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 移除前缀后的包含关系
|
||||
if strings.Contains(mainCode1, code2) || strings.Contains(code2, mainCode1) ||
|
||||
strings.Contains(code1, mainCode2) || strings.Contains(mainCode2, code1) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// extractResponseExample 提取产品响应示例数据(优先级:文档 > API配置 > 默认值)
|
||||
func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, product *entities.Product) interface{} {
|
||||
var responseData interface{}
|
||||
|
||||
// 1. 优先从产品文档中获取
|
||||
doc, err := g.docRepo.FindByProductID(ctx, product.ID)
|
||||
if err == nil && doc != nil && doc.ResponseExample != "" {
|
||||
// 尝试直接解析为JSON
|
||||
err := json.Unmarshal([]byte(doc.ResponseExample), &responseData)
|
||||
if err == nil {
|
||||
// g.logger.Debug("从产品文档中提取响应示例成功",
|
||||
// zap.String("product_id", product.ID),
|
||||
// zap.String("product_code", product.Code),
|
||||
// )
|
||||
return responseData
|
||||
}
|
||||
|
||||
// 如果解析失败,尝试从Markdown代码块中提取JSON
|
||||
extractedData := extractJSONFromMarkdown(doc.ResponseExample)
|
||||
if extractedData != nil {
|
||||
// g.logger.Debug("从Markdown代码块中提取响应示例成功",
|
||||
// zap.String("product_id", product.ID),
|
||||
// zap.String("product_code", product.Code),
|
||||
// )
|
||||
return extractedData
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果文档中没有,尝试从产品API配置中获取
|
||||
apiConfig, err := g.apiConfigRepo.FindByProductID(ctx, product.ID)
|
||||
if err == nil && apiConfig != nil && apiConfig.ResponseExample != "" {
|
||||
// API配置的响应示例通常是 JSON 字符串
|
||||
err := json.Unmarshal([]byte(apiConfig.ResponseExample), &responseData)
|
||||
if err == nil {
|
||||
// g.logger.Debug("从产品API配置中提取响应示例成功",
|
||||
// zap.String("product_id", product.ID),
|
||||
// zap.String("product_code", product.Code),
|
||||
// )
|
||||
return responseData
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 如果都没有,返回默认空对象
|
||||
g.logger.Warn("未找到响应示例数据,使用默认空对象",
|
||||
zap.String("product_id", product.ID),
|
||||
zap.String("product_code", product.Code),
|
||||
)
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
// extractJSONFromMarkdown 从Markdown代码块中提取JSON
|
||||
func extractJSONFromMarkdown(markdown string) interface{} {
|
||||
// 查找 ```json 代码块
|
||||
re := regexp.MustCompile("(?s)```json\\s*(.*?)\\s*```")
|
||||
matches := re.FindStringSubmatch(markdown)
|
||||
|
||||
if len(matches) > 1 {
|
||||
var jsonData interface{}
|
||||
err := json.Unmarshal([]byte(matches[1]), &jsonData)
|
||||
if err == nil {
|
||||
return jsonData
|
||||
}
|
||||
}
|
||||
|
||||
// 也尝试查找 ``` 代码块(可能是其他格式)
|
||||
re2 := regexp.MustCompile("(?s)```\\s*(.*?)\\s*```")
|
||||
matches2 := re2.FindStringSubmatch(markdown)
|
||||
if len(matches2) > 1 {
|
||||
var jsonData interface{}
|
||||
err := json.Unmarshal([]byte(matches2[1]), &jsonData)
|
||||
if err == nil {
|
||||
return jsonData
|
||||
}
|
||||
}
|
||||
|
||||
// 如果提取失败,返回 nil(由调用者决定默认值)
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateCacheKey 生成缓存键
|
||||
func (g *ExampleJSONGenerator) 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[:]) + ".json"
|
||||
}
|
||||
|
||||
// getCachedData 获取缓存数据
|
||||
func (g *ExampleJSONGenerator) getCachedData(cacheKey string) ([]byte, error) {
|
||||
// 确保缓存目录存在
|
||||
if err := os.MkdirAll(g.CacheDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建缓存目录失败: %w", err)
|
||||
}
|
||||
|
||||
cacheFilePath := filepath.Join(g.CacheDir, cacheKey)
|
||||
|
||||
// 检查文件是否存在
|
||||
fileInfo, err := os.Stat(cacheFilePath)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil // 文件不存在,但不是错误
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查文件是否过期
|
||||
if time.Since(fileInfo.ModTime()) > g.CacheTTL {
|
||||
// 文件过期,删除
|
||||
os.Remove(cacheFilePath)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
return os.ReadFile(cacheFilePath)
|
||||
}
|
||||
|
||||
// cacheData 缓存数据
|
||||
func (g *ExampleJSONGenerator) cacheData(cacheKey string, data []byte) error {
|
||||
// 确保缓存目录存在
|
||||
if err := os.MkdirAll(g.CacheDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建缓存目录失败: %w", err)
|
||||
}
|
||||
|
||||
cacheFilePath := filepath.Join(g.CacheDir, cacheKey)
|
||||
|
||||
// 写入文件
|
||||
return os.WriteFile(cacheFilePath, data, 0644)
|
||||
}
|
||||
1648
internal/shared/component_report/handler.go
Normal file
1648
internal/shared/component_report/handler.go
Normal file
File diff suppressed because it is too large
Load Diff
163
internal/shared/component_report/handler_fixed.go
Normal file
163
internal/shared/component_report/handler_fixed.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package component_report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
finance_entities "hyapi-server/internal/domains/finance/entities"
|
||||
financeRepositories "hyapi-server/internal/domains/finance/repositories"
|
||||
"hyapi-server/internal/domains/product/repositories"
|
||||
"hyapi-server/internal/shared/payment"
|
||||
)
|
||||
|
||||
// ComponentReportHandler 组件报告处理器
|
||||
type ComponentReportHandlerFixed struct {
|
||||
exampleJSONGenerator *ExampleJSONGenerator
|
||||
zipGenerator *ZipGenerator
|
||||
productRepo repositories.ProductRepository
|
||||
componentReportRepo repositories.ComponentReportRepository
|
||||
purchaseOrderRepo financeRepositories.PurchaseOrderRepository
|
||||
rechargeRecordRepo interface {
|
||||
Create(ctx context.Context, record finance_entities.RechargeRecord) (finance_entities.RechargeRecord, error)
|
||||
}
|
||||
alipayOrderRepo interface {
|
||||
Create(ctx context.Context, order finance_entities.AlipayOrder) (finance_entities.AlipayOrder, error)
|
||||
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*finance_entities.AlipayOrder, error)
|
||||
Update(ctx context.Context, order finance_entities.AlipayOrder) error
|
||||
}
|
||||
wechatOrderRepo interface {
|
||||
Create(ctx context.Context, order finance_entities.WechatOrder) (finance_entities.WechatOrder, error)
|
||||
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*finance_entities.WechatOrder, error)
|
||||
Update(ctx context.Context, order finance_entities.WechatOrder) error
|
||||
}
|
||||
aliPayService *payment.AliPayService
|
||||
wechatPayService *payment.WechatPayService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewComponentReportHandlerFixed 创建组件报告处理器(修复版)
|
||||
func NewComponentReportHandlerFixed(
|
||||
productRepo repositories.ProductRepository,
|
||||
docRepo repositories.ProductDocumentationRepository,
|
||||
apiConfigRepo repositories.ProductApiConfigRepository,
|
||||
componentReportRepo repositories.ComponentReportRepository,
|
||||
purchaseOrderRepo financeRepositories.PurchaseOrderRepository,
|
||||
rechargeRecordRepo interface {
|
||||
Create(ctx context.Context, record finance_entities.RechargeRecord) (finance_entities.RechargeRecord, error)
|
||||
},
|
||||
alipayOrderRepo interface {
|
||||
Create(ctx context.Context, order finance_entities.AlipayOrder) (finance_entities.AlipayOrder, error)
|
||||
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*finance_entities.AlipayOrder, error)
|
||||
Update(ctx context.Context, order finance_entities.AlipayOrder) error
|
||||
},
|
||||
wechatOrderRepo interface {
|
||||
Create(ctx context.Context, order finance_entities.WechatOrder) (finance_entities.WechatOrder, error)
|
||||
GetByOutTradeNo(ctx context.Context, outTradeNo string) (*finance_entities.WechatOrder, error)
|
||||
Update(ctx context.Context, order finance_entities.WechatOrder) error
|
||||
},
|
||||
aliPayService *payment.AliPayService,
|
||||
wechatPayService *payment.WechatPayService,
|
||||
logger *zap.Logger,
|
||||
) *ComponentReportHandlerFixed {
|
||||
exampleJSONGenerator := NewExampleJSONGenerator(productRepo, docRepo, apiConfigRepo, logger)
|
||||
zipGenerator := NewZipGenerator(logger)
|
||||
|
||||
return &ComponentReportHandlerFixed{
|
||||
exampleJSONGenerator: exampleJSONGenerator,
|
||||
zipGenerator: zipGenerator,
|
||||
productRepo: productRepo,
|
||||
componentReportRepo: componentReportRepo,
|
||||
purchaseOrderRepo: purchaseOrderRepo,
|
||||
rechargeRecordRepo: rechargeRecordRepo,
|
||||
alipayOrderRepo: alipayOrderRepo,
|
||||
wechatOrderRepo: wechatOrderRepo,
|
||||
aliPayService: aliPayService,
|
||||
wechatPayService: wechatPayService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPaymentStatusFixed 检查支付状态(修复版)
|
||||
func (h *ComponentReportHandlerFixed) CheckPaymentStatusFixed(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "用户未登录",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
orderID := c.Param("orderId")
|
||||
if orderID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "订单ID不能为空",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 根据订单ID查询下载记录
|
||||
download, err := h.componentReportRepo.GetDownloadByID(c.Request.Context(), orderID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询下载记录失败", zap.Error(err), zap.String("order_id", orderID))
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": 404,
|
||||
"message": "订单不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证订单是否属于当前用户
|
||||
if download.UserID != userID {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"code": 403,
|
||||
"message": "无权访问此订单",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 使用购买订单状态来判断支付状态
|
||||
var paymentStatus string
|
||||
var canDownload bool
|
||||
|
||||
// 查询购买订单状态
|
||||
purchaseOrder, err := h.purchaseOrderRepo.GetByID(c.Request.Context(), *download.OrderID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询购买订单失败", zap.Error(err), zap.String("OrderID", *download.OrderID))
|
||||
paymentStatus = "unknown"
|
||||
} else {
|
||||
// 根据购买订单状态设置支付状态
|
||||
switch purchaseOrder.Status {
|
||||
case finance_entities.PurchaseOrderStatusPaid:
|
||||
paymentStatus = "success"
|
||||
canDownload = true
|
||||
case finance_entities.PurchaseOrderStatusCreated:
|
||||
paymentStatus = "pending"
|
||||
canDownload = false
|
||||
case finance_entities.PurchaseOrderStatusCancelled:
|
||||
paymentStatus = "cancelled"
|
||||
canDownload = false
|
||||
case finance_entities.PurchaseOrderStatusFailed:
|
||||
paymentStatus = "failed"
|
||||
canDownload = false
|
||||
default:
|
||||
paymentStatus = "unknown"
|
||||
canDownload = false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if download.IsExpired() {
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, CheckPaymentStatusResponse{
|
||||
OrderID: download.ID,
|
||||
PaymentStatus: paymentStatus,
|
||||
CanDownload: canDownload,
|
||||
})
|
||||
}
|
||||
504
internal/shared/component_report/zip_generator.go
Normal file
504
internal/shared/component_report/zip_generator.go
Normal file
@@ -0,0 +1,504 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user