Files
tyapi-server/internal/shared/pdf/pdf_cache_manager.go
2026-01-27 16:26:48 +08:00

409 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package pdf
import (
"crypto/md5"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"go.uber.org/zap"
)
// PDFCacheManager PDF缓存管理器统一实现
// 支持两种缓存键生成方式:
// 1. 基于姓名+身份证用于PDFG报告
// 2. 基于产品ID+版本(用于产品文档)
type PDFCacheManager struct {
logger *zap.Logger
cacheDir string
ttl time.Duration // 缓存过期时间
maxSize int64 // 最大缓存大小字节0表示不限制
mu sync.RWMutex // 保护并发访问
cleanupOnce sync.Once // 确保清理任务只启动一次
}
// NewPDFCacheManager 创建PDF缓存管理器
// cacheDir: 缓存目录(空则使用默认目录)
// ttl: 缓存过期时间
// maxSize: 最大缓存大小字节0表示不限制
func NewPDFCacheManager(logger *zap.Logger, cacheDir string, ttl time.Duration, maxSize int64) (*PDFCacheManager, error) {
// 如果缓存目录为空使用项目根目录的storage/pdfg-cache目录
if cacheDir == "" {
wd, err := os.Getwd()
if err != nil {
cacheDir = filepath.Join(os.TempDir(), "tyapi_pdfg_cache")
} else {
cacheDir = filepath.Join(wd, "storage", "pdfg-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 生成缓存键(基于姓名+身份证)
func (m *PDFCacheManager) GetCacheKey(name, idCard string) string {
key := fmt.Sprintf("%s:%s", name, idCard)
hash := md5.Sum([]byte(key))
return hex.EncodeToString(hash[:])
}
// GetCacheKeyByProduct 生成缓存键基于产品ID+版本)
func (m *PDFCacheManager) GetCacheKeyByProduct(productID, version string) string {
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(name, idCard string) ([]byte, bool, time.Time, error) {
cacheKey := m.GetCacheKey(name, idCard)
return m.getByKey(cacheKey, name, idCard)
}
// GetByProduct 从缓存获取PDF文件基于产品ID+版本)
// 返回PDF字节流、是否命中缓存、错误
func (m *PDFCacheManager) GetByProduct(productID, version string) ([]byte, bool, error) {
cacheKey := m.GetCacheKeyByProduct(productID, version)
pdfBytes, hit, _, err := m.getByKey(cacheKey, productID, version)
return pdfBytes, hit, err
}
// getByKey 内部方法:根据缓存键获取文件
func (m *PDFCacheManager) getByKey(cacheKey string, key1, key2 string) ([]byte, bool, time.Time, error) {
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, time.Time{}, nil // 缓存未命中
}
return nil, false, time.Time{}, fmt.Errorf("检查缓存文件失败: %w", err)
}
// 检查文件是否过期从文件生成时间开始算24小时
createdAt := info.ModTime()
expiresAt := createdAt.Add(m.ttl)
if time.Now().After(expiresAt) {
// 缓存已过期,删除文件
m.logger.Debug("缓存已过期,删除文件",
zap.String("key1", key1),
zap.String("key2", key2),
zap.String("cache_key", cacheKey),
zap.Time("expires_at", expiresAt),
)
_ = os.Remove(cachePath)
return nil, false, time.Time{}, nil
}
// 读取缓存文件
pdfBytes, err := os.ReadFile(cachePath)
if err != nil {
return nil, false, time.Time{}, fmt.Errorf("读取缓存文件失败: %w", err)
}
m.logger.Debug("缓存命中",
zap.String("key1", key1),
zap.String("key2", key2),
zap.String("cache_key", cacheKey),
zap.Int64("file_size", int64(len(pdfBytes))),
zap.Time("expires_at", expiresAt),
)
return pdfBytes, true, createdAt, nil
}
// Set 将PDF文件保存到缓存基于姓名+身份证)
func (m *PDFCacheManager) Set(name, idCard string, pdfBytes []byte) error {
cacheKey := m.GetCacheKey(name, idCard)
return m.setByKey(cacheKey, pdfBytes, name, idCard)
}
// SetByProduct 将PDF文件保存到缓存基于产品ID+版本)
func (m *PDFCacheManager) SetByProduct(productID, version string, pdfBytes []byte) error {
cacheKey := m.GetCacheKeyByProduct(productID, version)
return m.setByKey(cacheKey, pdfBytes, productID, version)
}
// setByKey 内部方法:根据缓存键保存文件
func (m *PDFCacheManager) setByKey(cacheKey string, pdfBytes []byte, key1, key2 string) error {
cachePath := m.GetCachePath(cacheKey)
m.mu.Lock()
defer m.mu.Unlock()
// 检查缓存大小限制
if m.maxSize > 0 {
currentSize, err := m.getCacheDirSize()
if err != nil {
m.logger.Warn("获取缓存目录大小失败", zap.Error(err))
} else {
// 检查是否已存在文件
var oldFileSize int64
if info, err := os.Stat(cachePath); err == nil {
oldFileSize = info.Size()
}
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),
)
if err := m.cleanExpiredFiles(); 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 {
return fmt.Errorf("写入缓存文件失败: %w", err)
}
m.logger.Debug("PDF已保存到缓存",
zap.String("key1", key1),
zap.String("key2", key2),
zap.String("cache_key", cacheKey),
zap.Int64("file_size", int64(len(pdfBytes))),
)
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.cleanExpiredFiles(); err != nil {
m.logger.Warn("清理过期缓存文件失败", zap.Error(err))
}
}
}()
})
}
// cleanExpiredFiles 清理过期的缓存文件
func (m *PDFCacheManager) cleanExpiredFiles() error {
m.mu.Lock()
defer m.mu.Unlock()
entries, err := os.ReadDir(m.cacheDir)
if err != nil {
return fmt.Errorf("读取缓存目录失败: %w", err)
}
now := time.Now()
cleanedCount := 0
for _, entry := range entries {
if entry.IsDir() {
continue
}
// 只处理PDF文件
if filepath.Ext(entry.Name()) != ".pdf" {
continue
}
filePath := filepath.Join(m.cacheDir, entry.Name())
info, err := entry.Info()
if err != nil {
continue
}
// 检查文件是否过期
createdAt := info.ModTime()
expiresAt := createdAt.Add(m.ttl)
if now.After(expiresAt) {
if err := os.Remove(filePath); err != nil {
m.logger.Warn("删除过期缓存文件失败",
zap.String("file_path", filePath),
zap.Error(err),
)
} else {
cleanedCount++
}
}
}
if cleanedCount > 0 {
m.logger.Info("清理过期缓存文件完成",
zap.Int("cleaned_count", cleanedCount),
)
}
return nil
}
// Invalidate 使缓存失效基于产品ID+版本)
func (m *PDFCacheManager) Invalidate(productID, version string) error {
cacheKey := m.GetCacheKeyByProduct(productID, version)
cachePath := m.GetCachePath(cacheKey)
m.mu.Lock()
defer m.mu.Unlock()
if err := os.Remove(cachePath); err != nil {
if os.IsNotExist(err) {
return nil // 文件不存在,视为已失效
}
return fmt.Errorf("删除缓存文件失败: %w", err)
}
return nil
}
// InvalidateByNameIDCard 使缓存失效(基于姓名+身份证)
func (m *PDFCacheManager) InvalidateByNameIDCard(name, idCard string) error {
cacheKey := m.GetCacheKey(name, idCard)
cachePath := m.GetCachePath(cacheKey)
m.mu.Lock()
defer m.mu.Unlock()
if err := os.Remove(cachePath); err != nil {
if os.IsNotExist(err) {
return nil // 文件不存在,视为已失效
}
return fmt.Errorf("删除缓存文件失败: %w", err)
}
return nil
}
// Clear 清空所有缓存
func (m *PDFCacheManager) Clear() error {
m.mu.Lock()
defer m.mu.Unlock()
entries, err := os.ReadDir(m.cacheDir)
if err != nil {
return fmt.Errorf("读取缓存目录失败: %w", err)
}
count := 0
for _, entry := range entries {
if !entry.IsDir() && filepath.Ext(entry.Name()) == ".pdf" {
filePath := filepath.Join(m.cacheDir, entry.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()
entries, 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 _, entry := range entries {
if entry.IsDir() {
continue
}
if filepath.Ext(entry.Name()) == ".pdf" {
filePath := filepath.Join(m.cacheDir, entry.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
}