This commit is contained in:
475
internal/shared/pdf/pdf_cache_manager.go
Normal file
475
internal/shared/pdf/pdf_cache_manager.go
Normal file
@@ -0,0 +1,475 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user