add购买记录功能
This commit is contained in:
@@ -193,6 +193,13 @@ componentReportHandler := component_report.NewComponentReportHandler(
|
||||
productRepo,
|
||||
docRepo,
|
||||
apiConfigRepo,
|
||||
componentReportRepo,
|
||||
purchaseOrderRepo,
|
||||
rechargeRecordRepo,
|
||||
alipayOrderRepo,
|
||||
wechatOrderRepo,
|
||||
aliPayService,
|
||||
wechatPayService,
|
||||
logger,
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
102
internal/shared/component_report/check_payment_status_fix.go
Normal file
102
internal/shared/component_report/check_payment_status_fix.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package component_report
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
finance_entities "tyapi-server/internal/domains/finance/entities"
|
||||
)
|
||||
|
||||
// CheckPaymentStatusFixed 修复版检查支付状态方法
|
||||
func (h *ComponentReportHandler) 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
|
||||
|
||||
// 优先使用OrderID查询购买订单状态
|
||||
if download.OrderID != nil {
|
||||
// 查询购买订单状态
|
||||
purchaseOrder, err := h.purchaseOrderRepo.GetByID(c.Request.Context(), *download.OrderID)
|
||||
if err != nil {
|
||||
h.logger.Error("查询购买订单失败", zap.Error(err), zap.String("order_id", *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
|
||||
}
|
||||
}
|
||||
} else if download.OrderNumber != nil {
|
||||
// 兼容旧的支付订单逻辑
|
||||
paymentStatus = "success" // 简化处理,有支付订单号就认为已支付
|
||||
canDownload = true
|
||||
} else {
|
||||
paymentStatus = "pending"
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if download.IsExpired() {
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, CheckPaymentStatusResponse{
|
||||
OrderID: download.ID,
|
||||
PaymentStatus: paymentStatus,
|
||||
CanDownload: canDownload,
|
||||
})
|
||||
}
|
||||
@@ -2,12 +2,15 @@ package component_report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -21,6 +24,10 @@ type ExampleJSONGenerator struct {
|
||||
docRepo repositories.ProductDocumentationRepository
|
||||
apiConfigRepo repositories.ProductApiConfigRepository
|
||||
logger *zap.Logger
|
||||
// 缓存配置
|
||||
CacheEnabled bool
|
||||
CacheDir string
|
||||
CacheTTL time.Duration
|
||||
}
|
||||
|
||||
// NewExampleJSONGenerator 创建示例JSON生成器
|
||||
@@ -35,6 +42,30 @@ func NewExampleJSONGenerator(
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +85,20 @@ type ExampleJSONItem struct {
|
||||
// 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 {
|
||||
@@ -157,12 +202,21 @@ func (g *ExampleJSONGenerator) GenerateExampleJSON(ctx context.Context, productI
|
||||
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
|
||||
}
|
||||
|
||||
// MatchProductCodeToPath 根据产品编码匹配 UI 组件路径,返回路径和类型(folder/file)
|
||||
func (g *ExampleJSONGenerator) MatchProductCodeToPath(ctx context.Context, productCode string) (string, string, error) {
|
||||
basePath := filepath.Join("resources", "Pure Component", "src", "ui")
|
||||
// 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 {
|
||||
@@ -172,18 +226,8 @@ func (g *ExampleJSONGenerator) MatchProductCodeToPath(ctx context.Context, produ
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
|
||||
// 精确匹配
|
||||
if name == productCode {
|
||||
path := filepath.Join(basePath, name)
|
||||
fileType := "folder"
|
||||
if !entry.IsDir() {
|
||||
fileType = "file"
|
||||
}
|
||||
return path, fileType, nil
|
||||
}
|
||||
|
||||
// 模糊匹配:文件夹名称包含产品编号,或产品编号包含文件夹名称的核心部分
|
||||
if strings.Contains(name, productCode) || strings.Contains(productCode, extractCoreCode(name)) {
|
||||
// 使用改进的相似性匹配算法
|
||||
if isSimilarCode(subProductCode, name) {
|
||||
path := filepath.Join(basePath, name)
|
||||
fileType := "folder"
|
||||
if !entry.IsDir() {
|
||||
@@ -193,7 +237,7 @@ func (g *ExampleJSONGenerator) MatchProductCodeToPath(ctx context.Context, produ
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("未找到匹配的组件文件: %s", productCode)
|
||||
return "", "", fmt.Errorf("未找到匹配的组件文件: %s", subProductCode)
|
||||
}
|
||||
|
||||
// extractCoreCode 提取文件名中的核心编码部分
|
||||
@@ -206,6 +250,44 @@ func extractCoreCode(name string) string {
|
||||
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{}
|
||||
@@ -216,20 +298,20 @@ func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, produ
|
||||
// 尝试直接解析为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),
|
||||
)
|
||||
// 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),
|
||||
)
|
||||
// g.logger.Debug("从Markdown代码块中提取响应示例成功",
|
||||
// zap.String("product_id", product.ID),
|
||||
// zap.String("product_code", product.Code),
|
||||
// )
|
||||
return extractedData
|
||||
}
|
||||
}
|
||||
@@ -240,10 +322,10 @@ func (g *ExampleJSONGenerator) extractResponseExample(ctx context.Context, produ
|
||||
// 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),
|
||||
)
|
||||
// g.logger.Debug("从产品API配置中提取响应示例成功",
|
||||
// zap.String("product_id", product.ID),
|
||||
// zap.String("product_code", product.Code),
|
||||
// )
|
||||
return responseData
|
||||
}
|
||||
}
|
||||
@@ -284,3 +366,57 @@ func extractJSONFromMarkdown(markdown string) interface{} {
|
||||
// 如果提取失败,返回 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)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
172
internal/shared/component_report/handler_fixed.go
Normal file
172
internal/shared/component_report/handler_fixed.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package component_report
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
finance_entities "tyapi-server/internal/domains/finance/entities"
|
||||
financeRepositories "tyapi-server/internal/domains/finance/repositories"
|
||||
"tyapi-server/internal/domains/product/repositories"
|
||||
"tyapi-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
|
||||
|
||||
if download.OrderID != nil {
|
||||
// 查询购买订单状态
|
||||
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
|
||||
}
|
||||
}
|
||||
} else if download.OrderNumber != nil {
|
||||
// 兼容旧的支付订单逻辑
|
||||
paymentStatus = "success" // 简化处理,有支付订单号就认为已支付
|
||||
canDownload = true
|
||||
} else {
|
||||
paymentStatus = "pending"
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if download.IsExpired() {
|
||||
canDownload = false
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, CheckPaymentStatusResponse{
|
||||
OrderID: download.ID,
|
||||
PaymentStatus: paymentStatus,
|
||||
CanDownload: canDownload,
|
||||
})
|
||||
}
|
||||
@@ -3,11 +3,14 @@ package component_report
|
||||
import (
|
||||
"archive/zip"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -15,18 +18,35 @@ import (
|
||||
// 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,
|
||||
logger: logger,
|
||||
CacheEnabled: true,
|
||||
CacheDir: "storage/component-reports/cache",
|
||||
CacheTTL: 24 * time.Hour, // 默认缓存24小时
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateZipFile 生成ZIP文件,包含 example.json 和匹配的组件文件
|
||||
// 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: 子产品编号列表(如果为空,则处理所有子产品)
|
||||
// subProductCodes: 子产品编码列表(用于过滤和下载匹配的UI组件)
|
||||
// exampleJSONGenerator: 示例JSON生成器
|
||||
// outputPath: 输出ZIP文件路径(如果为空,则使用默认路径)
|
||||
func (g *ZipGenerator) GenerateZipFile(
|
||||
@@ -36,6 +56,29 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
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 {
|
||||
@@ -62,8 +105,8 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
zipWriter := zip.NewWriter(zipFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
// 4. 添加 example.json 到 public 目录
|
||||
exampleWriter, err := zipWriter.Create("public/example.json")
|
||||
// 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)
|
||||
}
|
||||
@@ -73,14 +116,14 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
return "", fmt.Errorf("写入example.json失败: %w", err)
|
||||
}
|
||||
|
||||
// 5. 添加整个 src 目录,但过滤 ui 目录下的文件
|
||||
srcBasePath := filepath.Join("resources", "Pure Component", "src")
|
||||
uiBasePath := filepath.Join(srcBasePath, "ui")
|
||||
// 5. 添加整个 Pure_Component 目录,但只包含子产品编码匹配的UI组件文件
|
||||
srcBasePath := filepath.Join("resources", "Pure_Component")
|
||||
uiBasePath := filepath.Join(srcBasePath, "src", "ui")
|
||||
|
||||
// 收集所有匹配的组件名称(文件夹名或文件名)
|
||||
// 根据子产品编码收集所有匹配的组件名称(文件夹名或文件名)
|
||||
matchedNames := make(map[string]bool)
|
||||
for _, productCode := range subProductCodes {
|
||||
path, _, err := exampleJSONGenerator.MatchProductCodeToPath(ctx, productCode)
|
||||
for _, subProductCode := range subProductCodes {
|
||||
path, _, err := exampleJSONGenerator.MatchSubProductCodeToPath(ctx, subProductCode)
|
||||
if err == nil && path != "" {
|
||||
// 获取组件名称(文件夹名或文件名)
|
||||
componentName := filepath.Base(path)
|
||||
@@ -88,20 +131,20 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历整个 src 目录
|
||||
// 遍历整个 Pure_Component 目录
|
||||
err = filepath.Walk(srcBasePath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 计算相对于 src 的路径
|
||||
// 计算相对于 Pure_Component 的路径
|
||||
relPath, err := filepath.Rel(srcBasePath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 转换为ZIP路径格式
|
||||
zipPath := filepath.ToSlash(filepath.Join("src", relPath))
|
||||
// 转换为ZIP路径格式,保持在Pure_Component目录下
|
||||
zipPath := filepath.ToSlash(filepath.Join("Pure_Component", relPath))
|
||||
|
||||
// 检查是否在 ui 目录下
|
||||
uiRelPath, err := filepath.Rel(uiBasePath, path)
|
||||
@@ -120,26 +163,19 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
// 获取文件/文件夹名称
|
||||
fileName := info.Name()
|
||||
|
||||
// 检查是否应该保留:
|
||||
// 1. CBehaviorRiskScan.vue 文件(无论在哪里)
|
||||
// 2. 匹配到的组件文件夹/文件
|
||||
// 检查是否应该保留:匹配到的组件文件夹/文件
|
||||
shouldInclude := false
|
||||
|
||||
// 检查是否是 CBehaviorRiskScan.vue
|
||||
if fileName == "CBehaviorRiskScan.vue" {
|
||||
// 检查是否是匹配的组件(检查组件名称)
|
||||
if matchedNames[fileName] {
|
||||
shouldInclude = true
|
||||
} else {
|
||||
// 检查是否是匹配的组件(检查组件名称)
|
||||
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
|
||||
}
|
||||
// 检查是否在匹配的组件文件夹内
|
||||
// 获取相对于 ui 的路径的第一部分(组件文件夹名)
|
||||
parts := strings.Split(filepath.ToSlash(uiRelPath), "/")
|
||||
if len(parts) > 0 && parts[0] != "" && parts[0] != "." {
|
||||
if matchedNames[parts[0]] {
|
||||
shouldInclude = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +200,7 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
g.logger.Warn("添加src目录失败", zap.Error(err))
|
||||
g.logger.Warn("添加Pure_Component目录失败", zap.Error(err))
|
||||
}
|
||||
|
||||
g.logger.Info("成功生成ZIP文件",
|
||||
@@ -174,6 +210,15 @@ func (g *ZipGenerator) GenerateZipFile(
|
||||
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
|
||||
}
|
||||
|
||||
@@ -263,3 +308,197 @@ func (g *ZipGenerator) AddFolderToZipWithPrefix(zipWriter *zip.Writer, folderPat
|
||||
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