423 lines
12 KiB
Go
423 lines
12 KiB
Go
package component_report
|
||
|
||
import (
|
||
"context"
|
||
"crypto/md5"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strings"
|
||
"time"
|
||
|
||
"go.uber.org/zap"
|
||
|
||
"tyapi-server/internal/domains/product/entities"
|
||
"tyapi-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)
|
||
}
|