package pdf import ( "crypto/md5" "encoding/hex" "fmt" "os" "path/filepath" "sync" "time" "go.uber.org/zap" ) // PDFCacheManager PDF缓存管理器 // 负责管理PDF文件的本地缓存,提高下载性能 type PDFCacheManager struct { logger *zap.Logger cacheDir string ttl time.Duration // 缓存过期时间 maxSize int64 // 最大缓存大小(字节) mu sync.RWMutex // 保护并发访问 cleanupOnce sync.Once // 确保清理任务只启动一次 } // CacheInfo 缓存信息 type CacheInfo struct { FilePath string Size int64 CreatedAt time.Time ExpiresAt time.Time ProductID string Version string } // NewPDFCacheManager 创建PDF缓存管理器 func NewPDFCacheManager(logger *zap.Logger, cacheDir string, ttl time.Duration, maxSize int64) (*PDFCacheManager, error) { // 如果缓存目录为空,使用默认目录 if cacheDir == "" { cacheDir = filepath.Join(os.TempDir(), "tyapi_pdf_cache") } // 确保缓存目录存在 if err := os.MkdirAll(cacheDir, 0755); err != nil { return nil, fmt.Errorf("创建缓存目录失败: %w", err) } manager := &PDFCacheManager{ logger: logger, cacheDir: cacheDir, ttl: ttl, maxSize: maxSize, } // 启动定期清理任务 manager.startCleanupTask() logger.Info("PDF缓存管理器已初始化", zap.String("cache_dir", cacheDir), zap.Duration("ttl", ttl), zap.Int64("max_size", maxSize), ) return manager, nil } // GetCacheKey 生成缓存键 // 基于产品ID和文档版本号生成唯一的缓存键 func (m *PDFCacheManager) GetCacheKey(productID string, version string) string { // 使用MD5哈希生成短键名 key := fmt.Sprintf("%s:%s", productID, version) hash := md5.Sum([]byte(key)) return hex.EncodeToString(hash[:]) } // GetCachePath 获取缓存文件路径 func (m *PDFCacheManager) GetCachePath(cacheKey string) string { return filepath.Join(m.cacheDir, fmt.Sprintf("%s.pdf", cacheKey)) } // Get 从缓存获取PDF文件 // 返回PDF字节流和是否命中缓存 func (m *PDFCacheManager) Get(productID string, version string) ([]byte, bool, error) { cacheKey := m.GetCacheKey(productID, version) cachePath := m.GetCachePath(cacheKey) m.mu.RLock() defer m.mu.RUnlock() // 检查文件是否存在 info, err := os.Stat(cachePath) if err != nil { if os.IsNotExist(err) { return nil, false, nil // 缓存未命中 } return nil, false, fmt.Errorf("检查缓存文件失败: %w", err) } // 检查文件是否过期 // 使用文件的修改时间作为创建时间 createdAt := info.ModTime() expiresAt := createdAt.Add(m.ttl) if time.Now().After(expiresAt) { // 缓存已过期,删除文件 m.logger.Debug("缓存已过期,删除文件", zap.String("product_id", productID), zap.String("cache_key", cacheKey), zap.Time("expires_at", expiresAt), ) _ = os.Remove(cachePath) return nil, false, nil } // 读取缓存文件 pdfBytes, err := os.ReadFile(cachePath) if err != nil { return nil, false, fmt.Errorf("读取缓存文件失败: %w", err) } m.logger.Debug("缓存命中", zap.String("product_id", productID), zap.String("cache_key", cacheKey), zap.Int64("file_size", int64(len(pdfBytes))), zap.Time("expires_at", expiresAt), ) return pdfBytes, true, nil } // Set 将PDF文件保存到缓存 func (m *PDFCacheManager) Set(productID string, version string, pdfBytes []byte) error { cacheKey := m.GetCacheKey(productID, version) cachePath := m.GetCachePath(cacheKey) m.mu.Lock() defer m.mu.Unlock() // 检查是否已存在缓存文件(用于判断是新建还是更新) fileExists := false var oldFileSize int64 if info, err := os.Stat(cachePath); err == nil { fileExists = true oldFileSize = info.Size() m.logger.Info("检测到已存在的缓存文件,将更新缓存", zap.String("product_id", productID), zap.String("version", version), zap.String("cache_key", cacheKey), zap.String("cache_path", cachePath), zap.Int64("old_file_size", oldFileSize), zap.Int64("new_file_size", int64(len(pdfBytes))), ) } else { m.logger.Info("开始创建新的PDF缓存", zap.String("product_id", productID), zap.String("version", version), zap.String("cache_key", cacheKey), zap.String("cache_path", cachePath), zap.Int64("file_size", int64(len(pdfBytes))), ) } // 检查缓存大小限制 if m.maxSize > 0 { currentSize, err := m.getCacheDirSize() if err != nil { m.logger.Warn("获取缓存目录大小失败", zap.Error(err)) } else { // 如果更新已存在的文件,需要减去旧文件的大小 sizeToAdd := int64(len(pdfBytes)) if fileExists { sizeToAdd = int64(len(pdfBytes)) - oldFileSize } if currentSize+sizeToAdd > m.maxSize { // 缓存空间不足,清理过期文件 m.logger.Warn("缓存空间不足,开始清理过期文件", zap.Int64("current_size", currentSize), zap.Int64("max_size", m.maxSize), zap.Int64("required_size", sizeToAdd), zap.Bool("is_update", fileExists), ) if err := m.cleanupExpiredFiles(); err != nil { m.logger.Warn("清理过期文件失败", zap.Error(err)) } // 再次检查 currentSize, _ = m.getCacheDirSize() if currentSize+sizeToAdd > m.maxSize { m.logger.Error("缓存空间不足,无法保存文件", zap.Int64("current_size", currentSize), zap.Int64("max_size", m.maxSize), zap.Int64("required_size", sizeToAdd), ) return fmt.Errorf("缓存空间不足,无法保存文件") } } } } // 写入缓存文件 if err := os.WriteFile(cachePath, pdfBytes, 0644); err != nil { m.logger.Error("写入缓存文件失败", zap.String("product_id", productID), zap.String("cache_path", cachePath), zap.Error(err), ) return fmt.Errorf("写入缓存文件失败: %w", err) } // 计算过期时间 expiresAt := time.Now().Add(m.ttl) // 根据是新建还是更新,记录不同的日志 if fileExists { m.logger.Info("PDF缓存已更新", zap.String("product_id", productID), zap.String("version", version), zap.String("cache_key", cacheKey), zap.String("cache_path", cachePath), zap.Int64("old_file_size", oldFileSize), zap.Int64("new_file_size", int64(len(pdfBytes))), zap.Int64("size_change", int64(len(pdfBytes))-oldFileSize), zap.Duration("ttl", m.ttl), zap.Time("expires_at", expiresAt), ) } else { m.logger.Info("PDF缓存已创建", zap.String("product_id", productID), zap.String("version", version), zap.String("cache_key", cacheKey), zap.String("cache_path", cachePath), zap.Int64("file_size", int64(len(pdfBytes))), zap.Duration("ttl", m.ttl), zap.Time("expires_at", expiresAt), ) } return nil } // Invalidate 使缓存失效 func (m *PDFCacheManager) Invalidate(productID string, version string) error { cacheKey := m.GetCacheKey(productID, version) cachePath := m.GetCachePath(cacheKey) m.logger.Info("开始使缓存失效", zap.String("product_id", productID), zap.String("version", version), zap.String("cache_key", cacheKey), zap.String("cache_path", cachePath), ) m.mu.Lock() defer m.mu.Unlock() // 检查文件是否存在,记录文件信息 fileInfo, err := os.Stat(cachePath) if err != nil { if os.IsNotExist(err) { m.logger.Info("缓存文件不存在,视为已失效", zap.String("product_id", productID), zap.String("cache_key", cacheKey), zap.String("cache_path", cachePath), ) return nil // 文件不存在,视为已失效 } m.logger.Error("检查缓存文件失败", zap.String("product_id", productID), zap.String("cache_path", cachePath), zap.Error(err), ) return fmt.Errorf("检查缓存文件失败: %w", err) } // 记录文件信息 fileSize := fileInfo.Size() fileModTime := fileInfo.ModTime() // 删除缓存文件 if err := os.Remove(cachePath); err != nil { m.logger.Error("删除缓存文件失败", zap.String("product_id", productID), zap.String("cache_path", cachePath), zap.Error(err), ) return fmt.Errorf("删除缓存文件失败: %w", err) } m.logger.Info("缓存已成功失效", zap.String("product_id", productID), zap.String("version", version), zap.String("cache_key", cacheKey), zap.String("cache_path", cachePath), zap.Int64("deleted_file_size", fileSize), zap.Time("file_created_at", fileModTime), ) return nil } // InvalidateByProductID 使指定产品的所有缓存失效 func (m *PDFCacheManager) InvalidateByProductID(productID string) error { m.mu.Lock() defer m.mu.Unlock() // 遍历缓存目录,查找匹配的文件 files, err := os.ReadDir(m.cacheDir) if err != nil { return fmt.Errorf("读取缓存目录失败: %w", err) } count := 0 for _, file := range files { if file.IsDir() { continue } // 读取文件内容,检查是否匹配产品ID // 由于我们使用MD5哈希,无法直接匹配,需要读取文件元数据 // 这里简化处理:删除所有PDF文件(实际应该存储元数据) // 更好的方案是使用数据库或JSON文件存储元数据 if filepath.Ext(file.Name()) == ".pdf" { filePath := filepath.Join(m.cacheDir, file.Name()) if err := os.Remove(filePath); err == nil { count++ } } } m.logger.Info("已清理产品缓存", zap.String("product_id", productID), zap.Int("deleted_count", count), ) return nil } // Clear 清空所有缓存 func (m *PDFCacheManager) Clear() error { m.mu.Lock() defer m.mu.Unlock() files, err := os.ReadDir(m.cacheDir) if err != nil { return fmt.Errorf("读取缓存目录失败: %w", err) } count := 0 for _, file := range files { if !file.IsDir() && filepath.Ext(file.Name()) == ".pdf" { filePath := filepath.Join(m.cacheDir, file.Name()) if err := os.Remove(filePath); err == nil { count++ } } } m.logger.Info("已清空所有缓存", zap.Int("deleted_count", count)) return nil } // GetCacheStats 获取缓存统计信息 func (m *PDFCacheManager) GetCacheStats() (map[string]interface{}, error) { m.mu.RLock() defer m.mu.RUnlock() files, err := os.ReadDir(m.cacheDir) if err != nil { return nil, fmt.Errorf("读取缓存目录失败: %w", err) } var totalSize int64 var fileCount int var expiredCount int now := time.Now() for _, file := range files { if file.IsDir() { continue } if filepath.Ext(file.Name()) == ".pdf" { filePath := filepath.Join(m.cacheDir, file.Name()) info, err := os.Stat(filePath) if err != nil { continue } totalSize += info.Size() fileCount++ // 检查是否过期 expiresAt := info.ModTime().Add(m.ttl) if now.After(expiresAt) { expiredCount++ } } } return map[string]interface{}{ "total_size": totalSize, "file_count": fileCount, "expired_count": expiredCount, "cache_dir": m.cacheDir, "ttl": m.ttl.String(), "max_size": m.maxSize, }, nil } // cleanupExpiredFiles 清理过期的缓存文件 func (m *PDFCacheManager) cleanupExpiredFiles() error { files, err := os.ReadDir(m.cacheDir) if err != nil { return fmt.Errorf("读取缓存目录失败: %w", err) } now := time.Now() count := 0 var totalFreed int64 for _, file := range files { if file.IsDir() { continue } if filepath.Ext(file.Name()) == ".pdf" { filePath := filepath.Join(m.cacheDir, file.Name()) info, err := os.Stat(filePath) if err != nil { continue } expiresAt := info.ModTime().Add(m.ttl) if now.After(expiresAt) { if err := os.Remove(filePath); err == nil { count++ totalFreed += info.Size() } } } } if count > 0 { m.logger.Info("已清理过期缓存文件", zap.Int("count", count), zap.Int64("freed_size", totalFreed), ) } return nil } // getCacheDirSize 获取缓存目录总大小 func (m *PDFCacheManager) getCacheDirSize() (int64, error) { var totalSize int64 err := filepath.Walk(m.cacheDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() { totalSize += info.Size() } return nil }) return totalSize, err } // startCleanupTask 启动定期清理任务 func (m *PDFCacheManager) startCleanupTask() { m.cleanupOnce.Do(func() { go func() { ticker := time.NewTicker(1 * time.Hour) // 每小时清理一次 defer ticker.Stop() for range ticker.C { if err := m.cleanupExpiredFiles(); err != nil { m.logger.Warn("定期清理缓存失败", zap.Error(err)) } } }() }) }