# 产品示例报告下载功能实现方案 ## 一、功能概述 在产品详情页面添加"下载示例报告"功能,允许用户下载与产品对应的前端组件报告。报告文件位于 `resources/Pure Component/src/ui` 目录下,通过产品编号(product_code)匹配对应的文件夹或文件。 ## 二、核心需求 ### 2.1 基本功能 1. **报告匹配**:根据子产品的 `product_code` 模糊匹配 `resources/Pure Component/src/ui` 下的文件夹或文件 - 支持前缀匹配(如产品编号为 `DWBG6A2C`,文件夹可能是 `DWBG6A2C` 或 `多cDWBG6A2C`) - 匹配规则:文件夹名称包含产品编号,或产品编号包含文件夹名称的核心部分 2. **文件打包**:将匹配到的文件夹或文件压缩成 ZIP 格式供用户下载 - ZIP 文件结构: ```text component-report.zip ├── src/ │ └── ui/ │ ├── DWBG6A2C/ # 子产品1的UI组件 │ │ ├── index.vue │ │ ├── components/ │ │ └── ... │ ├── CFLXG0V4B/ # 子产品2的UI组件 │ └── ... └── public/ └── example.json # 响应示例数据文件 ``` - 同时需要生成 `public/example.json` 文件,包含子产品的响应示例数据 - `example.json` 文件格式: ```json [ { "feature": { "featureName": "产品名称", "sort": 1 }, "data": { "apiID": "产品编号", "data": { // 子产品的响应示例数据(JSON对象) "code": 0, "message": "success", "data": { ... } } } }, { "feature": { "featureName": "另一个产品名称", "sort": 2 }, "data": { "apiID": "另一个产品编号", "data": { ... } } } ] ``` 3. **支付流程**: - 下载前检查用户已下载过的组件报告 - 根据子产品价格计算报告总价(已下载过的子产品价格需减免) - 用户扫码支付(微信/支付宝)成功后允许下载 ### 2.2 组合包产品特殊处理 1. **子产品筛选**:组合包产品只下载包含的子产品对应的报告 2. **价格减免**:如果用户已下载过某个子产品的报告,该子产品的价格不计入总价 3. **批量下载**:将多个子产品的报告打包成一个 ZIP 文件 ### 2.3 性能优化 1. **缓存机制**: - 缓存已生成的 ZIP 文件(基于产品ID和子产品列表的哈希值) - 缓存文件匹配结果(产品编号到文件夹路径的映射) - 缓存有效期:24小时 2. **二次下载**: - 用户已支付过的报告支持免费重新下载(在有效期内) - 记录下载历史,避免重复支付 ## 三、数据库设计 ### 3.1 报告下载记录表 (component_report_downloads) ```sql CREATE TABLE component_report_downloads ( id VARCHAR(36) PRIMARY KEY, user_id VARCHAR(36) NOT NULL, product_id VARCHAR(36) NOT NULL, product_code VARCHAR(50) NOT NULL, sub_product_ids TEXT, -- JSON数组,存储子产品ID列表(组合包使用) sub_product_codes TEXT, -- JSON数组,存储子产品编号列表 download_price DECIMAL(10,2) NOT NULL, -- 实际支付价格 original_price DECIMAL(10,2) NOT NULL, -- 原始总价 discount_amount DECIMAL(10,2) DEFAULT 0, -- 减免金额 payment_order_id VARCHAR(64), -- 支付订单号(关联充值记录) payment_type VARCHAR(20), -- 支付类型:alipay, wechat payment_status VARCHAR(20) DEFAULT 'pending', -- pending, success, failed file_path VARCHAR(500), -- 生成的ZIP文件路径(用于二次下载) file_hash VARCHAR(64), -- 文件哈希值(用于缓存验证) download_count INT DEFAULT 0, -- 下载次数 last_download_at TIMESTAMP, -- 最后下载时间 expires_at TIMESTAMP, -- 下载有效期(支付成功后30天) created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted_at TIMESTAMP NULL, INDEX idx_user_id (user_id), INDEX idx_product_id (product_id), INDEX idx_product_code (product_code), INDEX idx_payment_status (payment_status), INDEX idx_expires_at (expires_at) ); ``` ### 3.2 报告文件匹配缓存表 (component_report_cache) ```sql CREATE TABLE component_report_cache ( id VARCHAR(36) PRIMARY KEY, product_code VARCHAR(50) NOT NULL UNIQUE, matched_path VARCHAR(500) NOT NULL, -- 匹配到的文件夹/文件路径 file_type VARCHAR(20) NOT NULL, -- folder, file cache_key VARCHAR(64) NOT NULL UNIQUE, -- 缓存键(用于ZIP文件缓存) created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_product_code (product_code), INDEX idx_cache_key (cache_key) ); ``` ## 四、后端实现方案 ### 4.1 领域层 (Domain Layer) #### 4.1.1 实体定义 **文件位置**: `internal/domains/product/entities/component_report_download.go` ```go package entities import ( "time" "github.com/google/uuid" "github.com/shopspring/decimal" "gorm.io/gorm" ) // ComponentReportDownload 组件报告下载记录 type ComponentReportDownload struct { ID string `gorm:"primaryKey;type:varchar(36)"` UserID string `gorm:"type:varchar(36);not null;index"` ProductID string `gorm:"type:varchar(36);not null;index"` ProductCode string `gorm:"type:varchar(50);not null;index"` SubProductIDs string `gorm:"type:text"` // JSON数组 SubProductCodes string `gorm:"type:text"` // JSON数组 DownloadPrice decimal.Decimal `gorm:"type:decimal(10,2);not null"` OriginalPrice decimal.Decimal `gorm:"type:decimal(10,2);not null"` DiscountAmount decimal.Decimal `gorm:"type:decimal(10,2);default:0"` PaymentOrderID *string `gorm:"type:varchar(64)"` PaymentType *string `gorm:"type:varchar(20)"` PaymentStatus string `gorm:"type:varchar(20);default:'pending';index"` FilePath *string `gorm:"type:varchar(500)"` FileHash *string `gorm:"type:varchar(64)"` DownloadCount int `gorm:"default:0"` LastDownloadAt *time.Time ExpiresAt *time.Time `gorm:"index"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index"` } // ComponentReportCache 报告文件匹配缓存 type ComponentReportCache struct { ID string `gorm:"primaryKey;type:varchar(36)"` ProductCode string `gorm:"type:varchar(50);not null;uniqueIndex"` MatchedPath string `gorm:"type:varchar(500);not null"` FileType string `gorm:"type:varchar(20);not null"` CacheKey string `gorm:"type:varchar(64);not null;uniqueIndex"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` } ``` #### 4.1.2 仓储接口 **文件位置**: `internal/domains/product/repositories/component_report_repository_interface.go` ```go package repositories import ( "context" "tyapi-server/internal/domains/product/entities" ) // ComponentReportRepository 组件报告仓储接口 type ComponentReportRepository interface { // 创建下载记录 CreateDownload(ctx context.Context, download *entities.ComponentReportDownload) (*entities.ComponentReportDownload, error) // 更新下载记录 UpdateDownload(ctx context.Context, download *entities.ComponentReportDownload) error // 根据ID获取下载记录 GetDownloadByID(ctx context.Context, id string) (*entities.ComponentReportDownload, error) // 获取用户的下载记录列表 GetUserDownloads(ctx context.Context, userID string, productID *string) ([]*entities.ComponentReportDownload, error) // 检查用户是否已下载过指定产品 HasUserDownloaded(ctx context.Context, userID string, productCode string) (bool, error) // 获取用户已下载的产品编号列表 GetUserDownloadedProductCodes(ctx context.Context, userID string) ([]string, error) // 根据支付订单号获取下载记录 GetDownloadByPaymentOrderID(ctx context.Context, orderID string) (*entities.ComponentReportDownload, error) // 缓存相关 GetCacheByProductCode(ctx context.Context, productCode string) (*entities.ComponentReportCache, error) CreateCache(ctx context.Context, cache *entities.ComponentReportCache) error UpdateCache(ctx context.Context, cache *entities.ComponentReportCache) error } ``` #### 4.1.3 领域服务 **文件位置**: `internal/domains/product/services/component_report_service.go` ```go package services import ( "context" "path/filepath" "strings" "os" "tyapi-server/internal/domains/product/repositories" ) // ComponentReportService 组件报告服务 type ComponentReportService interface { // 匹配产品编号到文件路径 MatchProductCodeToPath(ctx context.Context, productCode string) (string, string, error) // 返回路径和类型 // 计算报告价格(考虑已下载的产品) CalculateReportPrice(ctx context.Context, userID string, productID string, subProductCodes []string) (decimal.Decimal, decimal.Decimal, []string, error) // 返回总价、减免金额、已下载的产品编号列表 // 生成 example.json 文件内容 GenerateExampleJSON(ctx context.Context, productID string, subProductCodes []string) ([]byte, error) // 生成ZIP文件 GenerateZipFile(ctx context.Context, productID string, subProductCodes []string, cacheKey string) (string, string, error) // 返回文件路径和哈希值 // 验证下载权限 ValidateDownloadPermission(ctx context.Context, userID string, downloadID string) (bool, error) } ``` ### 4.2 应用层 (Application Layer) **文件位置**: `internal/application/product/component_report_application_service.go` ```go package application import ( "context" "tyapi-server/internal/domains/product/services" "tyapi-server/internal/domains/finance/services" ) // ComponentReportApplicationService 组件报告应用服务 type ComponentReportApplicationService interface { // 1. 获取报告下载信息(价格、已下载列表等) GetReportDownloadInfo(ctx context.Context, userID string, productID string) (*ReportDownloadInfoResponse, error) // 2. 创建支付订单 CreateReportPaymentOrder(ctx context.Context, userID string, productID string) (*PaymentOrderResponse, error) // 3. 处理支付成功回调 HandlePaymentSuccess(ctx context.Context, orderID string) error // 4. 下载报告文件 DownloadReport(ctx context.Context, userID string, downloadID string) (*DownloadResponse, error) // 5. 获取用户下载历史 GetUserDownloadHistory(ctx context.Context, userID string, params *PaginationParams) (*DownloadHistoryResponse, error) } ``` ### 4.3 接口层 (Interface Layer) **文件位置**: `internal/infrastructure/http/handlers/component_report_handler.go` #### API 接口设计 1. **GET `/api/v1/products/{productId}/component-report/info`** - 获取报告下载信息(价格、已下载列表等) - 响应示例: ```json { "code": 200, "data": { "product_id": "xxx", "product_code": "DWBG6A2C", "is_package": true, "sub_products": [ { "product_id": "xxx", "product_code": "DWBG6A2C", "product_name": "产品名称", "price": 100.00, "is_downloaded": false, "matched": true, "has_response_example": true } ], "original_total_price": 300.00, "discount_amount": 100.00, "final_price": 200.00, "downloaded_product_codes": ["DWBG6A2C"], "includes_example_json": true } } ``` 2. **POST `/api/v1/products/{productId}/component-report/create-order`** - 创建支付订单 - 请求体: ```json { "payment_type": "wechat" // 或 "alipay" } ``` - 响应:返回支付二维码URL或支付链接 3. **POST `/api/v1/products/{productId}/component-report/payment-callback`** - 支付成功回调(内部调用,由支付服务触发) 4. **GET `/api/v1/products/{productId}/component-report/download/{downloadId}`** - 下载报告文件 - 响应:ZIP文件流 5. **GET `/api/v1/my/component-reports`** - 获取用户下载历史 - 查询参数:page, page_size, product_id ## 五、前端实现方案 ### 5.1 API 接口定义 **文件位置**: `tyapi-frontend/src/api/index.js` ```javascript // 组件报告相关接口 export const componentReportApi = { // 获取报告下载信息 getReportDownloadInfo: (productId) => request.get(`/products/${productId}/component-report/info`), // 创建支付订单 createReportPaymentOrder: (productId, data) => request.post(`/products/${productId}/component-report/create-order`, data), // 下载报告 downloadReport: (productId, downloadId) => request.get(`/products/${productId}/component-report/download/${downloadId}`, { responseType: 'blob' }), // 获取用户下载历史 getUserDownloadHistory: (params) => request.get('/my/component-reports', { params }) } ``` ### 5.2 页面组件修改 **文件位置**: `tyapi-frontend/src/pages/products/detail.vue` #### 5.2.1 添加下载按钮 在页面头部操作区域添加"下载示例报告"按钮: ```vue 下载示例报告 ``` #### 5.2.2 下载流程实现 ```javascript // 响应式数据 const reportDownloading = ref(false) const reportDownloadInfo = ref(null) const showReportPaymentDialog = ref(false) const reportPaymentType = ref('wechat') // 或 'alipay' // 获取报告下载信息 const loadReportDownloadInfo = async () => { if (!product.value) return try { const response = await componentReportApi.getReportDownloadInfo(product.value.id) reportDownloadInfo.value = response.data } catch (error) { console.error('获取报告下载信息失败:', error) } } // 处理下载报告 const handleDownloadReport = async () => { if (!product.value) return try { // 1. 获取下载信息 await loadReportDownloadInfo() // 2. 如果价格大于0,需要支付 if (reportDownloadInfo.value.final_price > 0) { showReportPaymentDialog.value = true return } // 3. 如果已支付过,直接下载 await downloadReportFile() } catch (error) { console.error('下载报告失败:', error) ElMessage.error('下载报告失败') } } // 创建支付订单并支付 const createReportPayment = async () => { if (!product.value) return try { const response = await componentReportApi.createReportPaymentOrder( product.value.id, { payment_type: reportPaymentType.value } ) // 显示支付二维码(参考现有的支付流程) if (reportPaymentType.value === 'wechat') { await showWechatQrCode(response.data.code_url) } else { // 支付宝支付 window.location.href = response.data.pay_url } // 开始轮询支付状态 startPaymentPolling(response.data.order_id) } catch (error) { console.error('创建支付订单失败:', error) ElMessage.error('创建支付订单失败') } } // 下载报告文件 const downloadReportFile = async (downloadId) => { if (!product.value) return reportDownloading.value = true try { // 如果没有downloadId,从下载信息中获取 const id = downloadId || reportDownloadInfo.value?.download_id if (!id) { ElMessage.warning('请先完成支付') return } const response = await componentReportApi.downloadReport(product.value.id, id) // 创建下载链接 const blob = new Blob([response.data], { type: 'application/zip' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = `${product.value.name || '产品'}_示例报告.zip` document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) ElMessage.success('报告下载成功') // 关闭支付对话框 showReportPaymentDialog.value = false } catch (error) { console.error('下载报告文件失败:', error) ElMessage.error('下载报告文件失败') } finally { reportDownloading.value = false } } ``` ## 六、核心算法实现 ### 6.1 响应示例数据提取 在生成 `example.json` 时,需要从产品文档中提取响应示例数据。数据来源优先级: 1. **产品文档的 `response_example` 字段**(JSON格式) 2. **产品文档的 `response_example` 字段**(Markdown代码块中的JSON) 3. **产品API配置的 `response_example` 字段**(如果产品文档中没有) 4. **默认空对象** `{}`(如果都没有) ### 6.2 产品编号匹配算法 ```go // MatchProductCodeToPath 匹配产品编号到文件路径 func (s *ComponentReportServiceImpl) MatchProductCodeToPath(ctx context.Context, productCode string) (string, string, error) { // 1. 先查缓存 cache, err := s.repo.GetCacheByProductCode(ctx, productCode) if err == nil && cache != nil { return cache.MatchedPath, cache.FileType, nil } // 2. 扫描目录 basePath := "resources/Pure Component/src/ui" entries, err := os.ReadDir(basePath) if err != nil { return "", "", err } // 3. 模糊匹配 for _, entry := range entries { name := entry.Name() // 精确匹配 if name == productCode { path := filepath.Join(basePath, name) fileType := "folder" if !entry.IsDir() { fileType = "file" } // 保存到缓存 cache := &entities.ComponentReportCache{ ID: uuid.New().String(), ProductCode: productCode, MatchedPath: path, FileType: fileType, CacheKey: generateCacheKey(productCode), } s.repo.CreateCache(ctx, cache) return path, fileType, nil } // 模糊匹配:文件夹名称包含产品编号,或产品编号包含文件夹名称的核心部分 if strings.Contains(name, productCode) || strings.Contains(productCode, extractCoreCode(name)) { path := filepath.Join(basePath, name) fileType := "folder" if !entry.IsDir() { fileType = "file" } // 保存到缓存 cache := &entities.ComponentReportCache{ ID: uuid.New().String(), ProductCode: productCode, MatchedPath: path, FileType: fileType, CacheKey: generateCacheKey(productCode), } s.repo.CreateCache(ctx, cache) return path, fileType, nil } } return "", "", fmt.Errorf("未找到匹配的报告文件: %s", productCode) } // extractCoreCode 提取文件夹名称的核心代码(去除前缀) func extractCoreCode(name string) string { // 如果名称以字母开头,提取后面的代码部分 // 例如:多cDWBG6A2C -> DWBG6A2C for i, r := range name { if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { return name[i:] } } return name } ``` ### 6.3 价格计算算法 ```go // CalculateReportPrice 计算报告价格 func (s *ComponentReportServiceImpl) CalculateReportPrice(ctx context.Context, userID string, productID string, subProductCodes []string) (decimal.Decimal, decimal.Decimal, []string, error) { // 1. 获取用户已下载的产品编号列表 downloadedCodes, err := s.repo.GetUserDownloadedProductCodes(ctx, userID) if err != nil { return decimal.Zero, decimal.Zero, nil, err } // 2. 获取产品信息(包括子产品价格) product, err := s.productRepo.GetByID(ctx, productID) if err != nil { return decimal.Zero, decimal.Zero, nil, err } // 3. 计算总价和减免金额 originalTotal := decimal.Zero discountAmount := decimal.Zero downloadedList := []string{} if product.IsPackage { // 组合包:遍历子产品 for _, item := range product.PackageItems { if contains(subProductCodes, item.ProductCode) { originalTotal = originalTotal.Add(item.Price) // 检查是否已下载 if contains(downloadedCodes, item.ProductCode) { discountAmount = discountAmount.Add(item.Price) downloadedList = append(downloadedList, item.ProductCode) } } } } else { // 单品 originalTotal = product.Price if contains(downloadedCodes, product.Code) { discountAmount = product.Price downloadedList = append(downloadedList, product.Code) } } finalPrice := originalTotal.Sub(discountAmount) return finalPrice, discountAmount, downloadedList, nil } ``` ### 6.4 example.json 文件生成算法 ```go // GenerateExampleJSON 生成 example.json 文件内容 func (s *ComponentReportServiceImpl) GenerateExampleJSON(ctx context.Context, productID string, subProductCodes []string) ([]byte, error) { // 1. 获取产品信息(包括子产品) product, err := s.productRepo.GetByID(ctx, productID) if err != nil { return nil, err } // 2. 构建 example.json 数组 var examples []map[string]interface{} if product.IsPackage { // 组合包:遍历子产品 for sort, item := range product.PackageItems { // 只处理在 subProductCodes 列表中的子产品 if !contains(subProductCodes, item.ProductCode) { continue } // 获取子产品的文档信息(包含响应示例) subProduct, err := s.productRepo.GetByID(ctx, item.ProductID) if err != nil { s.logger.Warn("获取子产品信息失败", zap.String("product_id", item.ProductID), zap.Error(err)) continue } // 获取响应示例数据(优先级:文档 > API配置 > 默认值) var responseData interface{} responseData = s.extractResponseExample(ctx, subProduct) // 构建示例项 example := map[string]interface{}{ "feature": map[string]interface{}{ "featureName": item.ProductName, "sort": sort + 1, }, "data": map[string]interface{}{ "apiID": item.ProductCode, "data": responseData, }, } examples = append(examples, example) } } else { // 单品 responseData := s.extractResponseExample(ctx, product) example := map[string]interface{}{ "feature": map[string]interface{}{ "featureName": product.Name, "sort": 1, }, "data": map[string]interface{}{ "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) } return jsonData, nil } // extractResponseExample 提取产品响应示例数据(优先级:文档 > API配置 > 默认值) func (s *ComponentReportServiceImpl) extractResponseExample(ctx context.Context, product *entities.Product) interface{} { var responseData interface{} // 1. 优先从产品文档中获取 if product.Documentation != nil && product.Documentation.ResponseExample != "" { // 尝试直接解析为JSON err := json.Unmarshal([]byte(product.Documentation.ResponseExample), &responseData) if err == nil { return responseData } // 如果解析失败,尝试从Markdown代码块中提取JSON responseData = extractJSONFromMarkdown(product.Documentation.ResponseExample) if responseData != nil { return responseData } } // 2. 如果文档中没有,尝试从产品API配置中获取 apiConfig, err := s.productApiConfigRepo.GetByProductID(ctx, product.ID) if err == nil && apiConfig != nil && apiConfig.ResponseExample != nil { // API配置的响应示例通常是 map[string]interface{} 类型 return apiConfig.ResponseExample } // 3. 如果都没有,返回默认空对象 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 } } // 如果提取失败,返回 nil(由调用者决定默认值) return nil } ``` ### 6.5 ZIP文件生成算法 ```go // GenerateZipFile 生成ZIP文件 func (s *ComponentReportServiceImpl) GenerateZipFile(ctx context.Context, productID string, subProductCodes []string, cacheKey string) (string, string, error) { // 1. 检查缓存文件是否存在 cacheDir := "storage/component-reports" zipPath := filepath.Join(cacheDir, cacheKey+".zip") if _, err := os.Stat(zipPath); err == nil { // 文件已存在,计算哈希值 hash, err := calculateFileHash(zipPath) if err == nil { return zipPath, hash, nil } } // 2. 创建ZIP文件 zipFile, err := os.Create(zipPath) if err != nil { return "", "", err } defer zipFile.Close() zipWriter := zip.NewWriter(zipFile) defer zipWriter.Close() // 3. 遍历子产品,添加UI组件文件到ZIP basePath := "resources/Pure Component/src/ui" for _, productCode := range subProductCodes { path, fileType, err := s.MatchProductCodeToPath(ctx, productCode) if err != nil { continue // 跳过未找到的文件 } if fileType == "folder" { // 递归添加文件夹 err = addFolderToZip(zipWriter, path, basePath) } else { // 添加单个文件 err = addFileToZip(zipWriter, path, basePath) } if err != nil { s.logger.Warn("添加文件到ZIP失败", zap.String("path", path), zap.Error(err)) } } // 4. 生成并添加 example.json 文件 exampleJSON, err := s.GenerateExampleJSON(ctx, productID, subProductCodes) if err != nil { s.logger.Warn("生成example.json失败", zap.Error(err)) // 不阻断流程,继续生成ZIP } else { // 添加 example.json 到 ZIP 的 public 目录 exampleWriter, err := zipWriter.Create("public/example.json") if err != nil { s.logger.Warn("创建example.json文件失败", zap.Error(err)) } else { _, err = exampleWriter.Write(exampleJSON) if err != nil { s.logger.Warn("写入example.json失败", zap.Error(err)) } } } // 5. 添加其他必要的文件(如果需要) // 例如:复制 public 目录下的其他文件(如果有) publicBasePath := "resources/Pure Component/public" publicFiles, err := os.ReadDir(publicBasePath) if err == nil { for _, file := range publicFiles { // 跳过 example.json(已经生成) if file.Name() == "example.json" { continue } filePath := filepath.Join(publicBasePath, file.Name()) if !file.IsDir() { err = addFileToZip(zipWriter, filePath, publicBasePath) if err != nil { s.logger.Warn("添加public文件失败", zap.String("file", file.Name()), zap.Error(err)) } } } } // 6. 计算文件哈希值 hash, err := calculateFileHash(zipPath) if err != nil { return "", "", err } return zipPath, hash, nil } ``` ## 七、支付流程集成 ### 7.1 支付订单创建 在创建报告支付订单时,需要: 1. 创建充值记录(RechargeRecord),类型为 `component_report` 2. 创建支付订单(微信/支付宝) 3. 创建下载记录(ComponentReportDownload),状态为 `pending` ### 7.2 支付成功回调 支付成功后: 1. 更新充值记录状态 2. 更新下载记录状态为 `success` 3. 生成ZIP文件(包括UI组件文件和 `public/example.json`) 4. 更新下载记录的 `file_path` 和 `file_hash` 5. 设置 `expires_at`(30天后) ### 7.3 二次下载 用户已支付过的报告: 1. 检查下载记录是否存在且未过期 2. 如果ZIP文件存在,直接返回 3. 如果ZIP文件不存在,重新生成(使用相同的cache_key) ## 八、性能优化策略 ### 8.1 缓存策略 1. **文件匹配缓存**:产品编号到文件路径的映射缓存到数据库 2. **ZIP文件缓存**:生成的ZIP文件保存到 `storage/component-reports` 目录 3. **缓存键生成**:基于产品ID和子产品编号列表生成唯一缓存键 ### 8.2 异步处理 对于大文件或包含多个子产品的组合包: 1. 支付成功后,异步生成ZIP文件 2. 生成完成后通知用户(可选:邮件/站内消息) 3. 用户下载时检查文件是否已生成完成 ### 8.3 文件清理 定期清理过期的ZIP文件: 1. 下载记录过期后(expires_at + 7天)删除ZIP文件 2. 使用定时任务(cron)定期执行清理 ## 九、错误处理 ### 9.1 文件未找到 - 如果某个子产品的报告文件未找到,记录警告日志,但不影响其他文件的打包 - 在响应中返回未找到的文件列表 ### 9.4 example.json 生成失败 - 如果某个子产品没有响应示例数据,使用空对象 `{}` 作为默认值 - 如果响应示例解析失败,尝试从Markdown代码块中提取JSON - 如果所有子产品都没有响应示例,生成包含空数据的 `example.json` 文件 - 记录警告日志,但不阻断ZIP文件生成流程 ### 9.2 支付失败 - 支付失败时,下载记录状态保持为 `pending` - 用户可以重新发起支付 ### 9.3 文件生成失败 - 记录错误日志 - 通知管理员 - 用户可以联系客服处理 ## 十、测试要点 1. **匹配算法测试**: - 精确匹配 - 模糊匹配(前缀、后缀) - 未找到文件的情况 2. **价格计算测试**: - 单品价格计算 - 组合包价格计算 - 已下载产品价格减免 3. **example.json 生成测试**: - 单品 example.json 生成 - 组合包 example.json 生成(多个子产品) - 响应示例数据提取(JSON格式、Markdown代码块格式) - 缺少响应示例时的默认值处理 4. **支付流程测试**: - 创建支付订单 - 支付成功回调 - 支付失败处理 5. **下载功能测试**: - 首次下载 - 二次下载 - 文件过期处理 6. **性能测试**: - 大文件生成时间 - 并发下载处理 - 缓存命中率 ## 十一、部署注意事项 1. **目录权限**:确保 `storage/component-reports` 目录有写权限 2. **存储空间**:ZIP文件可能较大,需要足够的磁盘空间 3. **备份策略**:定期备份下载记录和缓存表 4. **监控告警**:监控文件生成失败、支付回调异常等情况 ## 十二、后续优化方向 1. **CDN加速**:将ZIP文件上传到CDN,加速下载 2. **分片下载**:对于超大文件,支持分片下载 3. **预览功能**:在下载前提供报告预览 4. **版本管理**:支持报告版本更新,用户可选择下载历史版本 5. **批量下载**:支持用户选择多个产品批量下载