1002 lines
33 KiB
Markdown
1002 lines
33 KiB
Markdown
|
|
# 产品示例报告下载功能实现方案
|
|||
|
|
|
|||
|
|
## 一、功能概述
|
|||
|
|
|
|||
|
|
在产品详情页面添加"下载示例报告"功能,允许用户下载与产品对应的前端组件报告。报告文件位于 `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
|
|||
|
|
<el-button
|
|||
|
|
v-if="isSubscribed"
|
|||
|
|
:size="isMobile ? 'small' : 'default'"
|
|||
|
|
type="success"
|
|||
|
|
@click="handleDownloadReport"
|
|||
|
|
:loading="reportDownloading"
|
|||
|
|
>
|
|||
|
|
<el-icon><Download /></el-icon>
|
|||
|
|
下载示例报告
|
|||
|
|
</el-button>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 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. **批量下载**:支持用户选择多个产品批量下载
|